update 2026-05-13 01:55:45

This commit is contained in:
action committed 2026-05-13 01:55:45 +08:00
1 parent 1b6001bfed
commit 6f4a0231c5
1857 files changed
+749084 -99

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for FileBrowser
LUCI_PKGARCH:=all
PKG_VERSION:=1.1
PKG_RELEASE:=2
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,59 @@
-- Copyright 2018-2020 Lienol <lawlienol@gmail.com>
module("luci.controller.filebrowser", package.seeall)
local http = require "luci.http"
local api = require "luci.model.cbi.filebrowser.api"
function index()
if not nixio.fs.access("/etc/config/filebrowser") then
return
end
entry({"admin", "nas"}, firstchild(), "NAS", 44).dependent = false
entry({"admin", "nas", "filebrowser"}, cbi("filebrowser/settings"), _("File Browser"), 2).dependent = true
entry({"admin", "nas", "filebrowser", "check"}, call("action_check")).leaf = true
entry({"admin", "nas", "filebrowser", "download"}, call("action_download")).leaf = true
entry({"admin", "nas", "filebrowser", "status"}, call("act_status")).leaf = true
entry({"admin", "nas", "filebrowser", "get_log"}, call("get_log")).leaf = true
entry({"admin", "nas", "filebrowser", "clear_log"}, call("clear_log")).leaf = true
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write_json(content or {
code = 1
})
end
function act_status()
local e = {}
e.status = luci.sys.call("busybox ps -w | grep -v grep | grep 'filebrowser -a 0.0.0.0' >/dev/null") == 0
http_write_json(e)
end
function action_check()
local json = api.to_check()
http_write_json(json)
end
function action_download()
local json = nil
local task = http.formvalue("task")
if task == "extract" then
json = api.to_extract(http.formvalue("file"))
elseif task == "move" then
json = api.to_move(http.formvalue("file"))
else
json = api.to_download(http.formvalue("url"))
end
http_write_json(json)
end
function get_log()
luci.http.write(luci.sys.exec("[ -f '/var/log/filebrowser.log' ] && cat /var/log/filebrowser.log"))
end
function clear_log()
luci.sys.call("echo '' > /var/log/filebrowser.log")
end
@@ -0,0 +1,364 @@
local fs = require "nixio.fs"
local sys = require "luci.sys"
local uci = require"luci.model.uci".cursor()
local util = require "luci.util"
local i18n = require "luci.i18n"
module("luci.model.cbi.filebrowser.api", package.seeall)
local appname = "filebrowser"
local api_url = "https://api.github.com/repos/filebrowser/filebrowser/releases/latest"
local wget = "/usr/bin/wget"
local wget_args = {"--no-check-certificate", "--quiet", "--timeout=10", "--tries=2"}
local command_timeout = 300
local LEDE_BOARD = nil
local DISTRIB_TARGET = nil
function uci_get_type(type, config, default)
value = uci:get_first(appname, type, config, default) or
sys.exec("echo -n `uci -q get " .. appname .. ".@" .. type .. "[0]." .. config .. "`")
if (value == nil or value == "") and (default and default ~= "") then
value = default
end
return value
end
local function _unpack(t, i)
i = i or 1
if t[i] ~= nil then
return t[i], _unpack(t, i + 1)
end
end
local function exec(cmd, args, writer, timeout)
local os = require "os"
local nixio = require "nixio"
local fdi, fdo = nixio.pipe()
local pid = nixio.fork()
if pid > 0 then
fdo:close()
if writer or timeout then
local starttime = os.time()
while true do
if timeout and os.difftime(os.time(), starttime) >= timeout then
nixio.kill(pid, nixio.const.SIGTERM)
return 1
end
if writer then
local buffer = fdi:read(2048)
if buffer and #buffer > 0 then
writer(buffer)
end
end
local wpid, stat, code = nixio.waitpid(pid, "nohang")
if wpid and stat == "exited" then
return code
end
if not writer and timeout then
nixio.nanosleep(1)
end
end
else
local wpid, stat, code = nixio.waitpid(pid)
return wpid and stat == "exited" and code
end
elseif pid == 0 then
nixio.dup(fdo, nixio.stdout)
fdi:close()
fdo:close()
nixio.exece(cmd, args, nil)
nixio.stdout:close()
os.exit(1)
end
end
local function compare_versions(ver1, comp, ver2)
local table = table
local av1 = util.split(ver1, "[%.%-]", nil, true)
local av2 = util.split(ver2, "[%.%-]", nil, true)
local max = table.getn(av1)
local n2 = table.getn(av2)
if (max < n2) then
max = n2
end
for i = 1, max, 1 do
local s1 = av1[i] or ""
local s2 = av2[i] or ""
if comp == "~=" and (s1 ~= s2) then
return true
end
if (comp == "<" or comp == "<=") and (s1 < s2) then
return true
end
if (comp == ">" or comp == ">=") and (s1 > s2) then
return true
end
if (s1 ~= s2) then
return false
end
end
return not (comp == "<" or comp == ">")
end
local function auto_get_arch()
local arch = nixio.uname().machine or ""
if fs.access("/usr/lib/os-release") then
LEDE_BOARD = sys.exec("echo -n `grep 'LEDE_BOARD' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}'`")
end
if fs.access("/etc/openwrt_release") then
DISTRIB_TARGET = sys.exec("echo -n `grep 'DISTRIB_TARGET' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}'`")
end
if arch == "mips" then
if LEDE_BOARD and LEDE_BOARD ~= "" then
if string.match(LEDE_BOARD, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. LEDE_BOARD .. "' | grep -oE 'ramips|ar71xx'")
end
elseif DISTRIB_TARGET and DISTRIB_TARGET ~= "" then
if string.match(DISTRIB_TARGET, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. DISTRIB_TARGET .. "' | grep -oE 'ramips|ar71xx'")
end
end
end
return util.trim(arch)
end
local function get_file_info(arch)
local file_tree = ""
local sub_version = ""
if arch == "x86_64" then
file_tree = "amd64"
elseif arch == "aarch64" then
file_tree = "arm64"
elseif arch == "ramips" then
file_tree = "mipsle"
elseif arch == "ar71xx" then
file_tree = "mips"
elseif arch:match("^i[%d]86$") then
file_tree = "386"
elseif arch:match("^armv[5-8]") then
file_tree = "armv"
sub_version = arch:match("[5-8]")
if LEDE_BOARD and string.match(LEDE_BOARD, "bcm53xx") == "bcm53xx" then
sub_version = "5"
elseif DISTRIB_TARGET and string.match(DISTRIB_TARGET, "bcm53xx") == "bcm53xx" then
sub_version = "5"
end
sub_version = "5"
end
return file_tree, sub_version
end
local function get_api_json(url)
local jsonc = require "luci.jsonc"
local output = {}
-- exec(wget, { "-O-", url, _unpack(wget_args) },
-- function(chunk) output[#output + 1] = chunk end)
-- local json_content = util.trim(table.concat(output))
local json_content = luci.sys.exec(wget .. " --no-check-certificate --timeout=10 -t 1 -O- " .. url)
if json_content == "" then
return {}
end
return jsonc.parse(json_content) or {}
end
function get_version()
return uci_get_type("global", "version", "0")
end
function to_check(arch)
if not arch or arch == "" then
arch = auto_get_arch()
end
local file_tree, sub_version = get_file_info(arch)
if file_tree == "" then
return {
code = 1,
error = i18n.translate("Can't determine ARCH, or ARCH not supported.")
}
end
local json = get_api_json(api_url)
if json.tag_name == nil then
return {
code = 1,
error = i18n.translate("Get remote version info failed.")
}
end
local remote_version = json.tag_name:match("[^v]+")
local needs_update = compare_versions(get_version(), "<", remote_version)
local html_url, download_url
if needs_update then
html_url = json.html_url
for _, v in ipairs(json.assets) do
if v.name and v.name:match("linux%-" .. file_tree .. sub_version) then
download_url = v.browser_download_url
break
end
end
end
if needs_update and not download_url then
return {
code = 1,
version = remote_version,
html_url = html_url,
error = i18n.translate("New version found, but failed to get new version download url.")
}
end
return {
code = 0,
update = needs_update,
version = remote_version,
url = {
html = html_url,
download = download_url
}
}
end
function to_download(url)
if not url or url == "" then
return {
code = 1,
error = i18n.translate("Download url is required.")
}
end
sys.call("/bin/rm -f /tmp/filebrowser_download.*")
local tmp_file = util.trim(util.exec("mktemp -u -t filebrowser_download.XXXXXX"))
local result = exec(wget, {"-O", tmp_file, url, _unpack(wget_args)}, nil, command_timeout) == 0
if not result then
exec("/bin/rm", {"-f", tmp_file})
return {
code = 1,
error = i18n.translatef("File download failed or timed out: %s", url)
}
end
return {
code = 0,
file = tmp_file
}
end
function to_extract(file, subfix)
if not file or file == "" or not fs.access(file) then
return {
code = 1,
error = i18n.translate("File path required.")
}
end
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
local tmp_dir = util.trim(util.exec("mktemp -d -t filebrowser_extract.XXXXXX"))
local output = {}
exec("/bin/tar", {"-C", tmp_dir, "-zxvf", file}, function(chunk)
output[#output + 1] = chunk
end)
local files = util.split(table.concat(output))
exec("/bin/rm", {"-f", file})
if not new_file then
for _, f in pairs(files) do
if f:match("filebrowser") then
new_file = tmp_dir .. "/" .. util.trim(f)
break
end
end
end
if not new_file then
exec("/bin/rm", {"-rf", tmp_dir})
return {
code = 1,
error = i18n.translatef("Can't find client in file: %s", file)
}
end
return {
code = 0,
file = new_file
}
end
function to_move(file)
if not file or file == "" or not fs.access(file) then
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
return {
code = 1,
error = i18n.translate("Client file is required.")
}
end
local project_directory = uci_get_type("global", "project_directory", "/tmp")
luci.sys.exec("mkdir -p " .. project_directory)
local client_path = project_directory .. "/" .. appname
local client_path_bak
if fs.access(client_path) then
client_path_bak = "/tmp/" .. appname .. ".bak"
exec("/bin/mv", {"-f", client_path, client_path_bak})
end
local result = exec("/bin/mv", {"-f", file, client_path}, nil, command_timeout) == 0
if not result or not fs.access(client_path) then
if client_path_bak then
exec("/bin/mv", {"-f", client_path_bak, client_path})
end
return {
code = 1,
error = i18n.translatef("Can't move new file to path: %s", client_path)
}
end
exec("/bin/chmod", {"755", client_path})
if client_path_bak then
exec("/bin/rm", {"-f", client_path_bak})
end
sys.call("/bin/rm -rf /tmp/filebrowser_extract.*")
return {
code = 0
}
end
@@ -0,0 +1,36 @@
m = Map("filebrowser", translate("FileBrowser"), translate(
"File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"))
m:append(Template("filebrowser/status"))
s = m:section(TypedSection, "global", translate("Global Settings"))
s.anonymous = true
s.addremove = false
o = s:option(Flag, "enable", translate("Enable"))
o.rmempty = false
o = s:option(Value, "port", translate("Listen port"))
o.datatype = "port"
o.default = 8088
o.rmempty = false
o = s:option(Value, "root_path", translate("Root path"),
translate("Point to a path to access your files in the web interface, default is /"))
o.default = "/"
o.rmempty = false
o = s:option(Value, "project_directory", translate("Project directory"), translate(
"The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"))
o.default = "/tmp"
o.rmempty = false
o = s:option(Button, "_download", translate("Manually download"), translate(
"Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the project storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"))
o.template = "filebrowser/download"
o.inputstyle = "apply"
o.btnclick = "downloadClick(this);"
o.id = "download_btn"
m:append(Template("filebrowser/log"))
return m
@@ -0,0 +1,169 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">//<![CDATA[
var msgInfo;
var tokenStr = '<%=token%>';
var clickToDownloadText = '<%:Click to download%>';
var inProgressText = '<%:Downloading...%>';
var downloadInProgressNotice = '<%:Download, are you sure to close?%>';
var downloadSuccessText = '<%:Download successful%>';
var unexpectedErrorText = '<%:Unexpected error%>';
function addPageNotice() {
window.onbeforeunload = function(e) {
e.returnValue = downloadInProgressNotice;
return downloadInProgressNotice;
};
}
function removePageNotice() {
window.onbeforeunload = undefined;
}
function onUpdateSuccess(btn) {
alert(downloadSuccessText);
if (btn) {
btn.value = downloadSuccessText;
btn.placeholder = downloadSuccessText;
btn.disabled = true;
}
window.setTimeout(function () {
window.location.reload();
}, 1000);
}
function onRequestError(btn, errorMessage) {
btn.disabled = false;
btn.value = btn.placeholder;
if (errorMessage) {
alert(errorMessage);
}
}
function doAjaxGet(url, data, onResult) {
new XHR().get(url, data, function(_, json) {
var resultJson = json || {
'code': 1,
'error': unexpectedErrorText
};
if (typeof onResult === 'function') {
onResult(resultJson);
}
})
}
function downloadClick(btn) {
if (msgInfo === undefined) {
checkUpdate(btn);
} else {
doDownload(btn);
}
}
function checkUpdate(btn) {
btn.disabled = true;
btn.value = inProgressText;
addPageNotice();
var ckeckDetailElm = document.getElementById(btn.id + '-detail');
doAjaxGet('<%=dsp.build_url("admin/nas/filebrowser/check")%>/', {
token: tokenStr
}, function (json) {
removePageNotice();
if (json.code) {
eval('Info = undefined');
onRequestError(btn, json.error);
} else {
eval('Info = json');
btn.disabled = false;
btn.value = clickToDownloadText;
btn.placeholder = clickToDownloadText;
}
if (ckeckDetailElm) {
var urlNode = '';
if (json.version) {
urlNode = '<em style="color:red;"><%:The latest version:%>' + json.version + '</em>';
if (json.url && json.url.html) {
urlNode = '<a href="' + json.url.html + '" target="_blank">' + urlNode + '</a>';
}
}
ckeckDetailElm.innerHTML = urlNode;
}
msgInfo = json;
});
}
function doDownload(btn) {
btn.disabled = true;
btn.value = '<%:Downloading...%>';
addPageNotice();
var UpdateUrl = '<%=dsp.build_url("admin/nas/filebrowser/download")%>';
// Download file
doAjaxGet(UpdateUrl, {
token: tokenStr,
url: msgInfo ? msgInfo.url.download : ''
}, function (json) {
if (json.code) {
removePageNotice();
onRequestError(btn, json.error);
} else {
btn.value = '<%:Unpacking...%>';
// Extract file
doAjaxGet(UpdateUrl, {
token: tokenStr,
task: 'extract',
file: json.file
}, function (json) {
if (json.code) {
removePageNotice();
onRequestError(btn, json.error);
} else {
btn.value = '<%:Moving...%>';
// Move file to target dir
doAjaxGet(UpdateUrl, {
token: tokenStr,
task: 'move',
file: json.file
}, function (json) {
removePageNotice();
if (json.code) {
onRequestError(btn, json.error);
} else {
onUpdateSuccess(btn);
}
})
}
})
}
})
}
//]]></script>
<%+cbi/valueheader%>
<% if self:cfgvalue(section) ~= false then %>
<input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" type="button"<%=
attr("name", cbid) ..
attr("id", self.id or cbid) ..
attr("value", self.inputtitle or self.title) ..
ifattr(self.btnclick, "onclick", self.btnclick) ..
ifattr(self.placeholder, "placeholder")
%> />
<span id="<%=self.id or cbid%>-detail"></span>
<% else %>
-
<% end %>
<%+cbi/valuefooter%>
@@ -0,0 +1,31 @@
<script type="text/javascript">
//<![CDATA[
function clear_log(btn) {
XHR.get('<%=url([[admin]], [[nas]], [[filebrowser]], [[clear_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
XHR.poll(3, '<%=url([[admin]], [[nas]], [[filebrowser]], [[get_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<legend>
<%:Logs%>
</legend>
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>">
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="5" wrap="off" readonly="readonly"></textarea>
</fieldset>
@@ -0,0 +1,29 @@
<%
local dsp = require "luci.dispatcher"
-%>
<fieldset class="cbi-section">
<legend><%:Running Status%></legend>
<fieldset class="cbi-section">
<div class="cbi-value">
<label class="cbi-value-title"><%:Status%></label>
<div class="cbi-value-field" id="_status"><%:Collecting data...%></div>
</div>
</fieldset>
</fieldset>
<script type="text/javascript">//<![CDATA[
var _status = document.getElementById('_status');
XHR.poll(3,'<%=dsp.build_url("admin/nas/filebrowser/status")%>', null,
function(x, json) {
if (x && x.status == 200) {
if (_status)
_status.innerHTML = json.status ? '<font color="green"><%:RUNNING%> ✓</font><p><input type="button" class="cbi-button cbi-input-apply" value="<%:Enter interface%>" onclick="openwebui()" /></p>' : '<font color="red"><%:NOT RUNNING%> X</font>';
}
});
function openwebui(){
var url = window.location.host+":<%=luci.sys.exec("uci get filebrowser.@global[0].port"):gsub("^%s*(.-)%s*$", "%1")%>";
window.open('http://'+url,'target','');
}
//]]></script>
@@ -0,0 +1,107 @@
msgid "File Browser"
msgstr "文件浏览器"
msgid "File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"
msgstr "文件浏览器是一种创建你自己的云的软件,你可以在服务器上安装它,将它指向一个路径,然后通过一个漂亮的web界面访问你的文件。您有许多可用的特性!"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Enter interface"
msgstr "进入界面"
msgid "Global Settings"
msgstr "全局设置"
msgid "Enable"
msgstr "启用"
msgid "Listen port"
msgstr "监听端口"
msgid "Root path"
msgstr "指向路径"
msgid "Point to a path to access your files in the web interface, default is /"
msgstr "指向一个路径,可在web界面访问你的文件,默认为 /"
msgid "Project directory"
msgstr "项目存放目录"
msgid "The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"
msgstr "文件较大,至少需要32M空间。建议插入U盘或硬盘,或放入tmp目录里使用<br />例如:/mnt/sda1<br />例如:/tmp"
msgid "Manually download"
msgstr "手动下载"
msgid "Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the project storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"
msgstr "请确保具有足够的空间。<br /><font style='color:red'>第一次运行务必填好项目存放目录,然后保存应用。再手动下载,否则无法使用!</font>"
msgid "Logs"
msgstr "日志"
msgid "Clear logs"
msgstr "清空日志"
msgid "It is the latest version"
msgstr "已是最新版本"
msgid "Download successful"
msgstr "下载成功"
msgid "Click to download"
msgstr "点击下载"
msgid "Updating..."
msgstr "更新中"
msgid "Unexpected error"
msgstr "意外错误"
msgid "Download, are you sure to close?"
msgstr "正在下载,你确认要关闭吗?"
msgid "Downloading..."
msgstr "下载中"
msgid "Unpacking..."
msgstr "解压中"
msgid "Moving..."
msgstr "移动中"
msgid "The latest version:"
msgstr "最新版本:"
msgid "Can't determine ARCH, or ARCH not supported."
msgstr "无法确认ARCH架构,或是不支持。"
msgid "Get remote version info failed."
msgstr "获取远程版本信息失败。"
msgid "New version found, but failed to get new version download url."
msgstr "发现新版本,但未能获得新版本的下载地址。"
msgid "Download url is required."
msgstr "请指定下载地址。"
msgid "File download failed or timed out: %s"
msgstr "文件下载失败或超时:%s"
msgid "File path required."
msgstr "请指定文件路径。"
msgid "Can't find client in file: %s"
msgstr "无法在文件中找到客户端:%s"
msgid "Client file is required."
msgstr "请指定客户端文件。"
msgid "The client file is not suitable for current device."
msgstr "客户端文件不适合当前设备。"
msgid "Can't move new file to path: %s"
msgstr "无法移动新文件到:%s"
+1
View File
@@ -0,0 +1 @@
zh-cn
@@ -0,0 +1,7 @@
config global
option port '8088'
option root_path '/'
option project_directory '/tmp'
option enable '0'
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
START=99
LOG_PATH="/var/log/filebrowser.log"
echolog() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S"): $1" >> $LOG_PATH
}
config_t_get() {
local index=0
[ -n "$4" ] && index=$4
local ret=$(uci get filebrowser.@$1[$index].$2 2>/dev/null)
echo ${ret:=$3}
}
start() {
ENABLED=$(config_t_get global enable 0)
[ "$ENABLED" = "0" ] && return
PORT=$(config_t_get global port 8088)
ROOT_PATH=$(config_t_get global root_path /)
project_directory=$(config_t_get global project_directory /tmp)
[ ! -f "$project_directory/filebrowser" ] && echolog "在$project_directory找不到FileBrowser主程序,请下载。" && exit
export HOME="/root"
$project_directory/filebrowser -a 0.0.0.0 -p $PORT -r $ROOT_PATH -d ${ROOT_PATH}filebrowser.db -l $LOG_PATH >/dev/null 2>&1 &
}
stop() {
busybox ps -w | grep -v "grep" | grep "$project_directory/filebrowser -a 0.0.0.0" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 &
rm -rf $LOG_PATH
}
restart() {
stop
sleep 1
start
}
@@ -0,0 +1,11 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@filebrowser[-1]
add ucitrack filebrowser
set ucitrack.@filebrowser[-1].init=filebrowser
commit ucitrack
EOF
rm -rf /tmp/luci-*cache
exit 0
@@ -0,0 +1,11 @@
{
"luci-app-filebrowser": {
"description": "Grant UCI access for luci-app-filebrowser",
"read": {
"uci": [ "filebrowser" ]
},
"write": {
"uci": [ "filebrowser" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "filebrowser",
"init": "filebrowser"
}