feat(web-ui): add browse feature to find directories/executables/files (#4848)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user