diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp index c8db4ddcbbe5abcfc4f88c868186adf692e2fb1d..1797acb2b50ec90b5b46348a890d7bc2d7445687 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.cpp +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include @@ -36,338 +38,471 @@ namespace server { namespace { bool IsPathSafe(const std::string& path) { - //! 检查是否有".."路径组件,这可能导致目录遍历 - std::istringstream path_stream(path); - std::string component; + //! 检查是否有".."路径组件,这可能导致目录遍历 + std::istringstream path_stream(path); + std::string component; - while (std::getline(path_stream, component, '/')) { - if (component == "..") - return false; //! 不允许上级目录访问 - } + while (std::getline(path_stream, component, '/')) { + if (component == "..") + return false; //! 不允许上级目录访问 + } - return true; + return true; +} + +std::string GenerateETag(time_t mtime, off_t size) { + return "\"" + std::to_string(static_cast(mtime)) + + "-" + std::to_string(static_cast(size)) + "\""; +} + +std::string FormatHttpDate(time_t t) { + struct tm tm_buf; + gmtime_r(&t, &tm_buf); + char buf[64]; + strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf); + return buf; +} + +time_t ParseHttpDate(const std::string& s) { + struct tm t = {}; + if (strptime(s.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &t) == nullptr) + return -1; + return timegm(&t); +} + +std::string GetHeader(const tbox::http::Headers& headers, const std::string& lower_name) { + for (const auto& h : headers) { + if (tbox::util::string::ToLower(h.first) == lower_name) + return h.second; + } + return ""; } } //! 目录配置项 struct DirectoryConfig { - std::string url_prefix; //! URL前缀 - std::string local_path; //! 本地路径 - std::string default_file; //! 默认文件 + std::string url_prefix; //! URL前缀 + std::string local_path; //! 本地路径 + std::string default_file; //! 默认文件 }; //! 中间件私有数据结构 struct FileDownloaderMiddleware::Data { - eventx::WorkThread worker; - std::vector directories; //! 目录配置列表 - std::map path_mappings;//! 特定路径映射 - std::map mime_types; //! MIME类型映射 - std::string default_mime_type; //! 默认MIME类型 - bool directory_listing_enabled; //! 是否允许目录列表 - size_t switch_to_worker_filesize_threshold; - - Data(event::Loop *wp_loop) - : worker(wp_loop) - , default_mime_type("application/octet-stream") - , directory_listing_enabled(false) - , switch_to_worker_filesize_threshold(100 << 10) - { - //! 初始化常见MIME类型 - mime_types["html"] = "text/html"; - mime_types["htm"] = "text/html"; - mime_types["css"] = "text/css"; - mime_types["js"] = "application/javascript"; - mime_types["json"] = "application/json"; - mime_types["xml"] = "application/xml"; - mime_types["txt"] = "text/plain"; - mime_types["png"] = "image/png"; - mime_types["jpg"] = "image/jpeg"; - mime_types["jpeg"] = "image/jpeg"; - mime_types["gif"] = "image/gif"; - mime_types["webp"] = "image/webp"; - mime_types["svg"] = "image/svg+xml"; - mime_types["ico"] = "image/x-icon"; - mime_types["pdf"] = "application/pdf"; - mime_types["zip"] = "application/zip"; - mime_types["tar"] = "application/x-tar"; - mime_types["gz"] = "application/gzip"; - mime_types["mp3"] = "audio/mpeg"; - mime_types["mp4"] = "video/mp4"; - mime_types["woff"] = "font/woff"; - mime_types["woff2"] = "font/woff2"; - mime_types["ttf"] = "font/ttf"; - mime_types["otf"] = "font/otf"; - } + eventx::WorkThread worker; + std::vector directories; //! 目录配置列表 + std::map path_mappings;//! 特定路径映射 + std::map mime_types; //! MIME类型映射 + std::string default_mime_type; //! 默认MIME类型 + bool directory_listing_enabled; //! 是否允许目录列表 + size_t switch_to_worker_filesize_threshold; + + Data(event::Loop *wp_loop) + : worker(wp_loop) + , default_mime_type("application/octet-stream") + , directory_listing_enabled(false) + , switch_to_worker_filesize_threshold(100 << 10) + { + //! 初始化常见MIME类型 + mime_types["html"] = "text/html"; + mime_types["htm"] = "text/html"; + mime_types["css"] = "text/css"; + mime_types["js"] = "application/javascript"; + mime_types["json"] = "application/json"; + mime_types["xml"] = "application/xml"; + mime_types["txt"] = "text/plain"; + mime_types["png"] = "image/png"; + mime_types["jpg"] = "image/jpeg"; + mime_types["jpeg"] = "image/jpeg"; + mime_types["gif"] = "image/gif"; + mime_types["webp"] = "image/webp"; + mime_types["svg"] = "image/svg+xml"; + mime_types["ico"] = "image/x-icon"; + mime_types["pdf"] = "application/pdf"; + mime_types["zip"] = "application/zip"; + mime_types["tar"] = "application/x-tar"; + mime_types["gz"] = "application/gzip"; + mime_types["mp3"] = "audio/mpeg"; + mime_types["mp4"] = "video/mp4"; + mime_types["woff"] = "font/woff"; + mime_types["woff2"] = "font/woff2"; + mime_types["ttf"] = "font/ttf"; + mime_types["otf"] = "font/otf"; + } }; FileDownloaderMiddleware::FileDownloaderMiddleware(event::Loop *wp_loop) - : d_(new Data(wp_loop)) + : d_(new Data(wp_loop)) { } FileDownloaderMiddleware::~FileDownloaderMiddleware() { delete d_; } bool FileDownloaderMiddleware::addDirectory(const std::string& url_prefix, - const std::string& local_path, - const std::string& default_file) { - //! 验证URL前缀是否以'/'开头 - if (url_prefix.empty() || url_prefix[0] != '/') { - LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str()); - return false; - } - - //! 验证本地路径是否存在且是目录 - if (!util::fs::IsDirectoryExist(local_path)) { - LogErr("Invalid local path: %s. Directory does not exist", local_path.c_str()); - return false; - } - - //! 添加到目录列表 - DirectoryConfig config; - config.url_prefix = url_prefix; - config.local_path = local_path; - config.default_file = default_file; - - //! 确保本地路径以'/'结尾 - if (!config.local_path.empty() && config.local_path.back() != '/') - config.local_path += '/'; - - d_->directories.push_back(config); - LogInfo("Added directory mapping: %s -> %s", url_prefix.c_str(), local_path.c_str()); - return true; + const std::string& local_path, + const std::string& default_file) { + //! 验证URL前缀是否以'/'开头 + if (url_prefix.empty() || url_prefix[0] != '/') { + LogErr("Invalid URL prefix: %s. Must start with '/'", url_prefix.c_str()); + return false; + } + + //! 验证本地路径是否存在且是目录 + if (!util::fs::IsDirectoryExist(local_path)) { + LogErr("Invalid local path: %s. Directory does not exist", local_path.c_str()); + return false; + } + + //! 添加到目录列表 + DirectoryConfig config; + config.url_prefix = url_prefix; + config.local_path = local_path; + config.default_file = default_file; + + //! 确保本地路径以'/'结尾 + if (!config.local_path.empty() && config.local_path.back() != '/') + config.local_path += '/'; + + d_->directories.push_back(config); + LogInfo("Added directory mapping: %s -> %s", url_prefix.c_str(), local_path.c_str()); + return true; } void FileDownloaderMiddleware::setDirectoryListingEnabled(bool enable) { - d_->directory_listing_enabled = enable; + d_->directory_listing_enabled = enable; } void FileDownloaderMiddleware::setPathMapping(const std::string& url, const std::string& file) { - d_->path_mappings[url] = file; + d_->path_mappings[url] = file; } void FileDownloaderMiddleware::setDefaultMimeType(const std::string& mime_type) { - d_->default_mime_type = mime_type; + d_->default_mime_type = mime_type; } void FileDownloaderMiddleware::setMimeType(const std::string& ext, const std::string& mime_type) { - d_->mime_types[ext] = mime_type; + d_->mime_types[ext] = mime_type; } void FileDownloaderMiddleware::handle(ContextSptr sp_ctx, const NextFunc& next) { - const auto& request = sp_ctx->req(); + const auto& request = sp_ctx->req(); - //! 只处理GET和HEAD请求 - if (request.method != Method::kGet && request.method != Method::kHead) { - next(); + //! 处理 OPTIONS 预检请求(浏览器跨域访问) + if (request.method == Method::kOptions) { + auto& res = sp_ctx->res(); + res.status_code = StatusCode::k200_OK; + res.headers["Access-Control-Allow-Origin"] = "*"; + res.headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS"; + res.headers["Access-Control-Allow-Headers"] = "Auth, Range, If-Range, If-None-Match, If-Modified-Since"; + res.headers["Access-Control-Allow-Private-Network"] = "true"; + res.headers["Access-Control-Max-Age"] = "86400"; + return; + } + + //! 只处理GET和HEAD请求 + if (request.method != Method::kGet && request.method != Method::kHead) { + next(); + return; + } + + const std::string& request_path = request.url.path; + + //! 检查特定路径映射 + auto mapping_it = d_->path_mappings.find(request_path); + if (mapping_it != d_->path_mappings.end()) { + if (respondFile(sp_ctx, mapping_it->second)) + return; + } + + //! 查找匹配的目录配置 + for (const auto& dir : d_->directories) { + //! 检查URL是否以该目录前缀开头 + if (request_path.find(dir.url_prefix) == 0) { + //! 获取相对路径部分 + std::string rel_path = request_path.substr(dir.url_prefix.length()); + + //! 如果路径以'/'开头,去掉这个斜杠避免双斜杠 + if (!rel_path.empty() && rel_path[0] == '/') + rel_path = rel_path.substr(1); + + //! 构造本地文件路径 + std::string file_path = dir.local_path + rel_path; + + //! 检查路径安全性 + if (!IsPathSafe(file_path)) { + LogWarn("Unsafe path detected: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; return; - } + } + + auto file_type = util::fs::GetFileType(file_path); + //! 检查路径是否是目录 + if (file_type == util::fs::FileType::kDirectory) { + //! 如果是目录且路径不以'/'结尾,进行重定向 + if (!request_path.empty() && request_path.back() != '/') { + sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; + sp_ctx->res().headers["Location"] = request_path + "/"; + return; + } - const std::string& request_path = request.url.path; + //! 尝试访问默认文件 + std::string default_file_path = file_path + dir.default_file; + if (util::fs::GetFileType(default_file_path) == util::fs::FileType::kRegular) { + if (respondFile(sp_ctx, default_file_path)) + return; + } - //! 检查特定路径映射 - auto mapping_it = d_->path_mappings.find(request_path); - if (mapping_it != d_->path_mappings.end()) { - if (respondFile(sp_ctx, mapping_it->second)) + //! 如果允许目录列表,生成目录内容 + if (d_->directory_listing_enabled) { + if (respondDirectory(sp_ctx, file_path, request_path)) return; - } + } - //! 查找匹配的目录配置 - for (const auto& dir : d_->directories) { - //! 检查URL是否以该目录前缀开头 - if (request_path.find(dir.url_prefix) == 0) { - //! 获取相对路径部分 - std::string rel_path = request_path.substr(dir.url_prefix.length()); - - //! 如果路径以'/'开头,去掉这个斜杠避免双斜杠 - if (!rel_path.empty() && rel_path[0] == '/') - rel_path = rel_path.substr(1); - - //! 构造本地文件路径 - std::string file_path = dir.local_path + rel_path; - - //! 检查路径安全性 - if (!IsPathSafe(file_path)) { - LogWarn("Unsafe path detected: %s", file_path.c_str()); - sp_ctx->res().status_code = StatusCode::k403_Forbidden; - return; - } + //! 否则返回403 Forbidden + LogNotice("Directory listing disabled for: %s", file_path.c_str()); + sp_ctx->res().status_code = StatusCode::k403_Forbidden; + return; - auto file_type = util::fs::GetFileType(file_path); - //! 检查路径是否是目录 - if (file_type == util::fs::FileType::kDirectory) { - //! 如果是目录且路径不以'/'结尾,进行重定向 - if (!request_path.empty() && request_path.back() != '/') { - sp_ctx->res().status_code = StatusCode::k301_MovedPermanently; - sp_ctx->res().headers["Location"] = request_path + "/"; - return; - } - - //! 尝试访问默认文件 - std::string default_file_path = file_path + dir.default_file; - if (util::fs::GetFileType(default_file_path) == util::fs::FileType::kRegular) { - if (respondFile(sp_ctx, default_file_path)) - return; - } - - //! 如果允许目录列表,生成目录内容 - if (d_->directory_listing_enabled) { - if (respondDirectory(sp_ctx, file_path, request_path)) - return; - } - - //! 否则返回403 Forbidden - LogNotice("Directory listing disabled for: %s", file_path.c_str()); - sp_ctx->res().status_code = StatusCode::k403_Forbidden; - return; - - } else if (file_type == util::fs::FileType::kRegular) { - //! 如果是普通文件,直接响应文件内容 - if (respondFile(sp_ctx, file_path)) - return; - } - } + } else if (file_type == util::fs::FileType::kRegular) { + //! 如果是普通文件,直接响应文件内容 + if (respondFile(sp_ctx, file_path)) + return; + } } + } - //! 如果没有找到匹配的文件,传递给下一个中间件 - next(); + //! 如果没有找到匹配的文件,传递给下一个中间件 + next(); } std::string FileDownloaderMiddleware::getMimeType(const std::string& filename) const { - //! 查找最后一个点的位置 - size_t dot_pos = filename.find_last_of('.'); - if (dot_pos != std::string::npos) { - std::string ext = util::string::ToLower(filename.substr(dot_pos + 1)); - //! 在MIME类型映射中查找 - auto it = d_->mime_types.find(ext); - if (it != d_->mime_types.end()) - return it->second; - } - - //! 未找到匹配的MIME类型,返回默认值 - return d_->default_mime_type; + //! 查找最后一个点的位置 + size_t dot_pos = filename.find_last_of('.'); + if (dot_pos != std::string::npos) { + std::string ext = util::string::ToLower(filename.substr(dot_pos + 1)); + //! 在MIME类型映射中查找 + auto it = d_->mime_types.find(ext); + if (it != d_->mime_types.end()) + return it->second; + } + + //! 未找到匹配的MIME类型,返回默认值 + return d_->default_mime_type; } bool FileDownloaderMiddleware::respondFile(ContextSptr sp_ctx, const std::string& file_path) { - auto& res = sp_ctx->res(); + auto& res = sp_ctx->res(); - //! 打开文件 - std::ifstream file(file_path, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - res.status_code = StatusCode::k404_NotFound; + //! 用 stat() 获取文件元信息,同时验证文件是否存在 + struct stat file_stat; + if (::stat(file_path.c_str(), &file_stat) != 0) { + res.status_code = StatusCode::k404_NotFound; + return true; + } + + size_t file_size = static_cast(file_stat.st_size); + time_t file_mtime = file_stat.st_mtime; + std::string etag = GenerateETag(file_mtime, file_stat.st_size); + std::string last_modified = FormatHttpDate(file_mtime); + + res.headers["Content-Type"] = getMimeType(file_path); + res.headers["Accept-Ranges"] = "bytes"; + res.headers["ETag"] = etag; + res.headers["Last-Modified"] = last_modified; + res.headers["Cache-Control"] = "public, max-age=0, must-revalidate"; + res.headers["Access-Control-Allow-Origin"] = "*"; + res.headers["Access-Control-Expose-Headers"] = "Content-Range, Content-Length, ETag, Last-Modified"; + + //! 条件请求:If-None-Match 优先于 If-Modified-Since(RFC 7232 §6) + std::string if_none_match = GetHeader(sp_ctx->req().headers, "if-none-match"); + if (!if_none_match.empty()) { + if (if_none_match == etag) { + res.status_code = StatusCode::k304_NotModified; + return true; + } + } else { + std::string if_modified_since = GetHeader(sp_ctx->req().headers, "if-modified-since"); + if (!if_modified_since.empty()) { + time_t since = ParseHttpDate(if_modified_since); + if (since != -1 && file_mtime <= since) { + res.status_code = StatusCode::k304_NotModified; return true; + } + } + } + + //! 解析 Range 请求头(需在 HEAD 判断之前,确保非法 Range 返回 416) + size_t range_start = 0; + size_t range_end = file_size > 0 ? file_size - 1 : 0; + bool has_range = false; + + if (file_size > 0) { + for (const auto& h : sp_ctx->req().headers) { + if (util::string::ToLower(h.first) == "range") { + const std::string& range_str = h.second; + if (range_str.compare(0, 6, "bytes=") == 0) { + std::string range_val = range_str.substr(6); + auto dash_pos = range_val.find('-'); + if (dash_pos != std::string::npos) { + try { + std::string start_str = range_val.substr(0, dash_pos); + std::string end_str = range_val.substr(dash_pos + 1); + if (start_str.empty()) { + size_t suffix = std::stoull(end_str); + range_start = (suffix >= file_size) ? 0 : file_size - suffix; + range_end = file_size - 1; + } else { + range_start = std::stoull(start_str); + range_end = end_str.empty() ? file_size - 1 : std::stoull(end_str); + } + has_range = true; + } catch (...) { + //! 解析失败则忽略 Range,退化为全量响应 + } + } + } + break; + } } - res.headers["Content-Type"] = getMimeType(file_path); - - //! 获取文件大小 - size_t file_size = static_cast(file.tellg()); - file.seekg(0, std::ios::beg); - - //! 如果是HEAD请求,不返回内容 - if (sp_ctx->req().method == Method::kHead) { - res.status_code = StatusCode::k200_OK; - res.headers["Content-Length"] = std::to_string(file_size); - return true; + //! If-Range:ETag 不匹配时降级为全量响应,保证数据一致性 + if (has_range) { + std::string if_range = GetHeader(sp_ctx->req().headers, "if-range"); + if (!if_range.empty() && if_range != etag) { + has_range = false; + range_start = 0; + range_end = file_size - 1; + } } - //! 文件是否大于100KB - if (file_size < d_->switch_to_worker_filesize_threshold) { - //! 小文件就直接读了 - res.status_code = StatusCode::k200_OK; - res.headers["Content-Length"] = std::to_string(file_size); - //! 将文件内容读到body中去 - res.body = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), file_size); - - } else { - //! 文件太大就采用子线程去读 - d_->worker.execute( - [sp_ctx, file_path] { - auto& res = sp_ctx->res(); - if (util::fs::ReadBinaryFromFile(file_path, res.body)) { - res.status_code = StatusCode::k200_OK; - res.headers["Content-Length"] = std::to_string(res.body.size()); - LogInfo("Served file: %s (%zu bytes)", file_path.c_str(), res.body.size()); - } else { - res.status_code = StatusCode::k404_NotFound; - } - }, - [sp_ctx] { } //! 这是为了确保sp_ctx在主线程上析构 - ); + //! 校验范围合法性 + if (has_range && (range_start >= file_size || range_end >= file_size || range_start > range_end)) { + res.status_code = StatusCode::k416_RequestedRangeNotSatisfiable; + res.headers["Content-Range"] = "bytes */" + std::to_string(file_size); + return true; } + } - return true; -} + size_t content_length = file_size > 0 ? range_end - range_start + 1 : 0; + res.headers["Content-Length"] = std::to_string(content_length); -bool FileDownloaderMiddleware::respondDirectory(ContextSptr sp_ctx, - const std::string& dir_path, - const std::string& url_path) { - try { - //! 生成HTML目录列表 - std::stringstream html; - html << "\n" - << "\n" - << "\n" - << " Directory listing for " << url_path << "\n" - << " \n" - << "\n" - << "\n" - << "

Directory listing for " << url_path << "

\n" - << "
    \n"; - - //! 如果不是根目录,添加返回上级目录的链接 - if (url_path != "/") { - size_t last_slash = url_path.find_last_of('/', url_path.size() - 2); - if (last_slash != std::string::npos) { - std::string parent_url = url_path.substr(0, last_slash + 1); - html << "
  • ..
  • \n"; - } - } + if (has_range) { + res.status_code = StatusCode::k206_PartialContent; + res.headers["Content-Range"] = "bytes " + std::to_string(range_start) + "-" + + std::to_string(range_end) + "/" + std::to_string(file_size); + } else { + res.status_code = StatusCode::k200_OK; + } - //! 列出目录中的项目 - std::vector entries; - if (!util::fs::ListDirectory(dir_path, entries)) { - LogErr("Failed to list directory: %s", dir_path.c_str()); - return false; - } + //! HEAD 请求不返回 body + if (sp_ctx->req().method == Method::kHead) + return true; - for (const auto& name : entries) { - std::string entry_path = dir_path + "/" + name; - std::string href = url_path + name; + //! 空文件直接返回 + if (file_size == 0) + return true; - auto entry_type = util::fs::GetFileType(entry_path); - if (entry_type == util::fs::FileType::kDirectory) { - href += "/"; - html << "
  • " << name << "/
  • \n"; - } else { - html << "
  • " << name << "
  • \n"; - } + if (content_length < d_->switch_to_worker_filesize_threshold) { + std::ifstream file(file_path, std::ios::binary); + if (!file.is_open()) { + res.status_code = StatusCode::k500_InternalServerError; + return true; + } + file.seekg(static_cast(range_start)); + res.body.resize(content_length); + file.read(&res.body[0], static_cast(content_length)); + LogInfo("Served file: %s (bytes %zu-%zu/%zu)", + file_path.c_str(), range_start, range_end, file_size); + } else { + d_->worker.execute( + [sp_ctx, file_path, range_start, content_length] { + auto& res = sp_ctx->res(); + std::ifstream f(file_path, std::ios::binary); + if (f.is_open()) { + f.seekg(static_cast(range_start)); + res.body.resize(content_length); + f.read(&res.body[0], static_cast(content_length)); + LogInfo("Served file(worker): %s (%zu bytes from %zu)", + file_path.c_str(), content_length, range_start); + } else { + res.status_code = StatusCode::k500_InternalServerError; } + }, + [sp_ctx] { } //! 确保 sp_ctx 在主线程上析构 + ); + } - html << "
\n" - << "\n" - << ""; + return true; +} - //! 设置响应 - auto& res = sp_ctx->res(); - res.status_code = StatusCode::k200_OK; - res.headers["Content-Type"] = "text/html; charset=utf-8"; - res.body = html.str(); +bool FileDownloaderMiddleware::respondDirectory(ContextSptr sp_ctx, + const std::string& dir_path, + const std::string& url_path) { + try { + //! 生成HTML目录列表 + std::stringstream html; + html << "\n" + << "\n" + << "\n" + << " Directory listing for " << url_path << "\n" + << " \n" + << "\n" + << "\n" + << "

Directory listing for " << url_path << "

\n" + << "
    \n"; + + //! 如果不是根目录,添加返回上级目录的链接 + if (url_path != "/") { + size_t last_slash = url_path.find_last_of('/', url_path.size() - 2); + if (last_slash != std::string::npos) { + std::string parent_url = url_path.substr(0, last_slash + 1); + html << "
  • ..
  • \n"; + } + } - LogInfo("Served directory listing for: %s", dir_path.c_str()); - return true; + //! 列出目录中的项目 + std::vector entries; + if (!util::fs::ListDirectory(dir_path, entries)) { + LogErr("Failed to list directory: %s", dir_path.c_str()); + return false; + } - } catch (const std::exception& e) { - LogErr("Failed to generate directory listing: %s", e.what()); - return false; + for (const auto& name : entries) { + std::string entry_path = dir_path + "/" + name; + std::string href = url_path + name; + + auto entry_type = util::fs::GetFileType(entry_path); + if (entry_type == util::fs::FileType::kDirectory) { + href += "/"; + html << "
  • " << name << "/
  • \n"; + } else { + html << "
  • " << name << "
  • \n"; + } } + + html << "
\n" + << "\n" + << ""; + + //! 设置响应 + auto& res = sp_ctx->res(); + res.status_code = StatusCode::k200_OK; + res.headers["Content-Type"] = "text/html; charset=utf-8"; + res.body = html.str(); + + LogInfo("Served directory listing for: %s", dir_path.c_str()); + return true; + + } catch (const std::exception& e) { + LogErr("Failed to generate directory listing: %s", e.what()); + return false; + } } }