feat(web-ui): add browse feature to find directories/executables/files (#4848)

This commit is contained in:
David Lane
2026-03-13 22:55:04 -04:00
committed by GitHub
parent b3f0e2370c
commit e836354e5a
7 changed files with 1113 additions and 10 deletions

View File

@@ -1462,6 +1462,193 @@ namespace confighttp {
send_response(response, output_tree);
}
/**
* @brief Checks whether a directory entry qualifies as an executable file.
* @param entry The directory entry to check.
* @param status The cached file status for the entry.
* @return True if the file should be included in an executable-type listing.
*/
bool is_browsable_executable([[maybe_unused]] const fs::directory_entry &entry, [[maybe_unused]] const fs::file_status &status) {
#ifdef _WIN32
auto ext = entry.path().extension().string();
boost::algorithm::to_lower(ext);
return ext == ".exe" || ext == ".bat" || ext == ".cmd" || ext == ".com" || ext == ".ps1";
#else
const auto perms = status.permissions();
return (perms & fs::perms::owner_exec) != fs::perms::none ||
(perms & fs::perms::group_exec) != fs::perms::none ||
(perms & fs::perms::others_exec) != fs::perms::none;
#endif
}
#ifdef _WIN32
/**
* @brief Builds a JSON array of available Windows drive letters.
* @return JSON array of drive-letter entries.
*/
nlohmann::json get_windows_drives() {
nlohmann::json entries = nlohmann::json::array();
const DWORD drives = GetLogicalDrives();
for (int i = 0; i < 26; ++i) {
if (drives & (1 << i)) {
const auto drive_letter = static_cast<char>('A' + i);
const auto drive_path = std::string(1, drive_letter) + ":\\";
nlohmann::json entry;
entry["name"] = drive_path;
entry["type"] = "directory";
entry["path"] = drive_path;
entries.push_back(entry);
}
}
return entries;
}
#endif
/**
* @brief Lists, filters, and sorts the entries of a directory for the browse API.
* @param dir_path The directory to list.
* @param type_str Filter type: "directory", "executable", "file", or "any".
* @return Sorted JSON array of entry objects with name/type/path fields.
*/
nlohmann::json build_browse_entries(const fs::path &dir_path, const std::string &type_str) {
nlohmann::json entries = nlohmann::json::array();
std::error_code iter_ec;
for (auto it = fs::directory_iterator(dir_path, fs::directory_options::skip_permission_denied, iter_ec);
!iter_ec && it != fs::directory_iterator();
it.increment(iter_ec)) {
try {
const auto status = it->status();
const bool is_dir = fs::is_directory(status);
if (const bool is_regular = fs::is_regular_file(status); !is_dir && !is_regular) {
continue;
}
// Apply type filter (directories are always included for navigation)
if (type_str == "directory" && !is_dir) {
continue;
}
if (type_str == "executable" && !is_dir && !is_browsable_executable(*it, status)) {
continue;
}
nlohmann::json file_entry;
file_entry["name"] = it->path().filename().string();
file_entry["path"] = it->path().string();
file_entry["type"] = is_dir ? "directory" : "file";
entries.push_back(file_entry);
} catch (const fs::filesystem_error &e) {
BOOST_LOG(debug) << "BrowseDirectory: skipping entry due to error: "sv << e.what();
}
}
if (iter_ec) {
BOOST_LOG(debug) << "BrowseDirectory: directory iteration error: "sv << iter_ec.message();
}
// Sort: directories first, then files; both case-insensitively alphabetical
std::sort(entries.begin(), entries.end(), [](const nlohmann::json &a, const nlohmann::json &b) {
const bool a_dir = (a["type"] == "directory");
if (const bool b_dir = (b["type"] == "directory"); a_dir != b_dir) {
return a_dir && !b_dir;
}
auto a_name = a["name"].get<std::string>();
auto b_name = b["name"].get<std::string>();
boost::algorithm::to_lower(a_name);
boost::algorithm::to_lower(b_name);
return a_name < b_name;
});
return entries;
}
/**
* @brief Browse the server filesystem.
* @param response The HTTP response object.
* @param request The HTTP request object.
* @note On Windows, an empty or root path returns the list of available drive letters.
* @note On non-Windows, an empty path defaults to the filesystem root ("/").
*
* @api_examples{/api/browse?path=/home/user&type=directory| GET| null}
*/
void browseDirectory(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
print_req(request);
try {
const auto query_params = request->parse_query_string();
std::string path_str;
if (const auto path_it = query_params.find("path"); path_it != query_params.end()) {
path_str = path_it->second;
}
std::string type_str = "any";
if (const auto type_it = query_params.find("type"); type_it != query_params.end() && !type_it->second.empty()) {
type_str = type_it->second;
}
nlohmann::json output_tree;
#ifdef _WIN32
// On Windows with an empty or root path, return the list of available drive letters
if (path_str.empty() || path_str == "/" || path_str == "\\") {
output_tree["path"] = "";
output_tree["parent"] = "";
output_tree["entries"] = get_windows_drives();
send_response(response, output_tree);
return;
}
#else
// On non-Windows, default an empty path to the filesystem root
if (path_str.empty()) {
path_str = "/";
}
#endif
// Normalize the path
fs::path dir_path = fs::weakly_canonical(fs::path(path_str));
// If the path points to a file, use its parent directory
std::error_code ec;
if (fs::is_regular_file(dir_path, ec)) {
dir_path = dir_path.parent_path();
}
// If the path doesn't exist, try the parent
if (!fs::exists(dir_path, ec)) {
dir_path = dir_path.parent_path();
}
if (!fs::is_directory(dir_path, ec)) {
bad_request(response, request, "Path is not a directory");
return;
}
output_tree["path"] = dir_path.string();
// Determine the parent path for the "Up" navigation
const fs::path parent = dir_path.parent_path();
#ifdef _WIN32
// At a drive root (e.g., C:\) the parent equals itself; signal the drive list with an empty string
output_tree["parent"] = (parent == dir_path) ? "" : parent.string();
#else
output_tree["parent"] = parent.string();
#endif
output_tree["entries"] = build_browse_entries(dir_path, type_str);
send_response(response, output_tree);
} catch (const fs::filesystem_error &e) {
BOOST_LOG(warning) << "BrowseDirectory: "sv << e.what();
bad_request(response, request, e.what());
}
}
void start() {
platf::set_thread_name("confighttp");
const auto shutdown_event = mail::man->event<bool>(mail::shutdown);
@@ -1505,6 +1692,7 @@ namespace confighttp {
server.resource["^/welcome/?$"]["GET"] = page_handler("welcome.html", false, true);
// rest api
server.resource["^/api/browse$"]["GET"] = browseDirectory;
server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/apps$"]["POST"] = saveApp;
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;

View File

@@ -5,6 +5,7 @@
#pragma once
// standard includes
#include <filesystem>
#include <memory>
#include <string>
@@ -42,8 +43,34 @@ namespace confighttp {
bool check_app_index(const resp_https_t &response, const req_https_t &request, int index);
void getPage(const resp_https_t &response, const req_https_t &request, const char *html_file, bool require_auth = true, bool redirect_if_username = false);
void getAsset(const resp_https_t &response, const req_https_t &request);
void browseDirectory(const resp_https_t &response, const req_https_t &request);
void getLocale(const resp_https_t &response, const req_https_t &request);
void getCSRFToken(const resp_https_t &response, const req_https_t &request);
// Browse helper functions (also exposed for unit testing)
/**
* @brief Checks whether a directory entry qualifies as an executable file.
* @param entry The directory entry to check.
* @param status The cached file status for the entry.
* @return True if the file should be included in an executable-type listing.
*/
bool is_browsable_executable(const std::filesystem::directory_entry &entry, const std::filesystem::file_status &status);
/**
* @brief Lists, filters, and sorts the entries of a directory for the browse API.
* @param dir_path The directory to list.
* @param type_str Filter type: "directory", "executable", "file", or "any".
* @return Sorted JSON array of entry objects with name/type/path fields.
*/
nlohmann::json build_browse_entries(const std::filesystem::path &dir_path, const std::string &type_str);
#ifdef _WIN32
/**
* @brief Builds a JSON array of available Windows drive letters.
* @return JSON array of drive-letter entries.
*/
nlohmann::json get_windows_drives();
#endif
} // namespace confighttp
// mime types map