#include "mergerfs_webui.hpp" #include "fs_exists.hpp" #include "fs_mounts.hpp" #include "mergerfs_api.hpp" #include "str.hpp" #include "CLI11.hpp" #include "fmt/core.h" #include "httplib.h" #include "json.hpp" #include using json = nlohmann::json; static json _generate_error(const fs::path &mount_, const std::string &key_, const std::string &val_, const int err_) { json rv; rv = json::object(); rv["mount"] = mount_.string(); rv["key"] = key_; rv["value"] = val_; switch(err_) { case -EROFS: rv["msg"] = fmt::format("'{}' is read only.",key_); break; case -EINVAL: rv["msg"] = fmt::format("value '{}' is invalid for '{}'", val_, key_); break; case -EACCES: rv["msg"] = fmt::format("mergerfs.webui (pid {}) is running as uid {}" " which appears not to have access to modify the" " mount's config.", ::getpid(), ::getuid()); break; case -ENOTCONN: rv["msg"] = fmt::format("It appears the mergerfs mount '{}' is in a bad state." " mergerfs may have crashed.", mount_.string()); break; default: rv["msg"] strerror(-err_); break; } return rv; } static void _get_root(const httplib::Request &req_, httplib::Response &res_) { std::string html; if(fs::exists("index.html")) { res_.set_file_content("index.html"); return; } html = R"html( mergerfs ui
)html"; res_.set_content(html, "text/html"); } #define IERT(S) if(type_ == (S)) return true; static bool _valid_fs_type(const std::string &type_) { IERT("ext2"); IERT("ext3"); IERT("ext4"); IERT("xfs"); IERT("jfs"); IERT("btrfs"); IERT("zfs"); IERT("reiserfs"); IERT("f2fs"); IERT("ntfs"); IERT("vfat"); IERT("exfat"); if(str::startswith(type_,"fuse.")) return true; return false; } static void _get_mounts(const httplib::Request &req_, httplib::Response &res_) { json json_array; std::string type; fs::MountVec mounts; fs::mounts(mounts); json_array = json::array(); for(const auto &mount : mounts) { if(not ::_valid_fs_type(mount.type)) continue; json obj; obj["path"] = mount.dir; obj["type"] = mount.type; json_array.push_back(obj); } res_.set_content(json_array.dump(), "application/json"); } static void _get_mounts_mergerfs(const httplib::Request &req_, httplib::Response &res_) { json json_array; std::string type; fs::MountVec mounts; fs::mounts(mounts); json_array = json::array(); for(const auto &mount : mounts) { if(mount.type != "fuse.mergerfs") continue; json_array.push_back(mount.dir); } res_.set_content(json_array.dump(), "application/json"); } static void _get_kvs(const httplib::Request &req_, httplib::Response &res_) { if(not req_.has_param("mount")) { res_.status = 400; res_.set_content("mount param not set", "text/plain"); return; } std::string mount; std::map kvs; mount = req_.get_param_value("mount"); mergerfs::api::get_kvs(mount,&kvs); j = kvs; res_.set_content(j.dump(), "application/json"); } static void _get_kvs_key(const httplib::Request &req_, httplib::Response &res_) { if(not req_.has_param("mount")) { res_.status = 400; res_.set_content("mount param not set", "text/plain"); return; } json j; fs::path mount; std::string key; std::string val; key = req_.path_params.at("key"); mount = req_.get_param_value("mount"); mergerfs::api::get_kv(mount,key,&val); j = val; res_.set_content(j.dump(), "application/json"); } static void _execute_command(const httplib::Request &req_, httplib::Response &res_) { if(not req_.has_param("mount")) { res_.status = 400; res_.set_content("{\"error\":\"mount param not set\"}", "application/json"); return; } fs::path mount; std::string cmd; mount = req_.get_param_value("mount"); cmd = req_.path_params.at("command"); int rv = mergerfs::api::set_kv(mount,"cmd." + cmd,""); if(rv >= 0) { res_.set_content("{\"result\":\"success\",\"message\":\"Command executed\"}", "application/json"); } else { res_.status = 400; res_.set_content("{\"result\":\"error\",\"message\":\"Failed to execute command\"}", "application/json"); } } static void _post_kvs_key(const httplib::Request &req_, httplib::Response &res_) { if(not req_.has_param("mount")) { res_.status = 400; res_.set_content("{\"error\":\"mount param not set\"}", "application/json"); return; } int rv; fs::path mount; std::string key; std::string val; key = req_.path_params.at("key"); val = json::parse(req_.body); mount = req_.get_param_value("mount"); rv = mergerfs::api::set_kv(mount,key,val); if(rv >= 0) { res_.set_content("{\"result\":\"success\"}", "application/json"); } else { res_.status = 400; res_.set_content("{\"result\":\"error\",\"message\":\"Failed to set configuration\"}", "application/json"); } } int mergerfs::webui::main(const int argc_, char **argv_) { CLI::App app; std::string host; int port; std::string password; app.description("mergerfs.webui:" " A simple webui to configure mergerfs instances"); app.name("USAGE: mergerfs.webui"); app.add_option("--host",host) ->description("Interface to bind to") ->default_val("0.0.0.0"); app.add_option("--port",port) ->description("TCP port to use") ->default_val(8080); app.add_option("--password",password) ->description(""); try { app.parse(argc_,argv_); } catch(const CLI::ParseError &e) { return app.exit(e); } // TODO: Warn if uid of server is not root but mergerfs is. httplib::Server http_server; http_server.Get("/",::_get_root); http_server.Get("/mounts",::_get_mounts); http_server.Get("/mounts/mergerfs",::_get_mounts_mergerfs); http_server.Get("/kvs",::_get_kvs); http_server.Get("/kvs/:key",::_get_kvs_key); http_server.Post("/kvs/:key",::_post_kvs_key); // New endpoints for enhanced UI http_server.Get("/branches",::_get_branches); http_server.Post("/branches",::_post_branches); http_server.Get("/xattr/:key",::_get_xattr); http_server.Post("/xattr/:key",::_post_xattr); http_server.Post("/cmd/:command",::_execute_command); std::cout << "host:port = http://" << host << ":" << port << std::endl; http_server.listen(host,port); return 0; }