#include "Global.h" #include "Performance_Monitor.h" #include #include #include #include #include #include #include #include /** * 瓦片文件缓存策略: * * 首次请求通过 newFileResponse() 直接发送文件,避免先把整个文件读取到内存。 * * 根据文件修改时间和大小生成弱 ETag,同时返回 Last-Modified。 * * 浏览器缓存未过期时直接使用本地缓存,不再请求服务器。 * * 缓存过期或强制刷新时,浏览器自动携带 If-None-Match 或 If-Modified-Since。 * * 服务端优先验证 If-None-Match,文件未变化时返回 304,不重复传输文件内容。 * * 404 响应使用 no-store,避免不存在的瓦片被浏览器长期缓存。 * * max-age=31536000, immutable 仅适用于同一 URL 对应的文件永远不被原地修改; * 如果瓦片可能在原路径更新,应移除 immutable 并使用较短的 max-age。 */ namespace { struct File_Cache_Info { std::uint64_t size; std::time_t modified_time; std::string etag; std::string last_modified; }; std::string format_http_date(std::time_t time) { std::tm tm{}; #ifdef _WIN32 gmtime_s(&tm, &time); #else gmtime_r(&time, &tm); #endif char buffer[64]; auto size = std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm); return std::string(buffer, size); } std::optional get_file_cache_info(const std::string& path) { auto native_path = std::filesystem::path(path); #ifdef _WIN32 struct _stati64 info{}; if (_wstati64(native_path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG) { return std::nullopt; } #else struct stat info{}; if (::stat(native_path.c_str(), &info) != 0 || !S_ISREG(info.st_mode)) { return std::nullopt; } #endif File_Cache_Info result; result.size = static_cast(info.st_size); result.modified_time = info.st_mtime; result.etag = std::format( "W/\"{:x}-{:x}\"", static_cast(result.modified_time), result.size); result.last_modified = format_http_date(result.modified_time); return result; } std::string_view trim_http_ows(std::string_view value) { while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { value.remove_prefix(1); } while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.remove_suffix(1); } return value; } std::string_view remove_weak_prefix(std::string_view etag) { if (etag.starts_with("W/")) { etag.remove_prefix(2); } return etag; } bool etag_matches(std::string_view values, std::string_view current_etag) { while (!values.empty()) { auto pos = values.find(','); auto value = trim_http_ows(values.substr(0, pos)); if (value == "*" || remove_weak_prefix(value) == remove_weak_prefix(current_etag)) { return true; } if (pos == std::string_view::npos) { break; } values.remove_prefix(pos + 1); } return false; } void add_tile_cache_headers( const drogon::HttpResponsePtr& response, const File_Cache_Info& info) { response->addHeader("Cache-Control", "public, max-age=86400"); //response->addHeader("Cache-Control", "public, max-age=31536000, immutable"); response->addHeader("ETag", info.etag); response->addHeader("Last-Modified", info.last_modified); } drogon::HttpResponsePtr make_tile_not_found_response(const std::string& path) { auto response = drogon::HttpResponse::newHttpResponse(); JSON ret = JSON::object(); ret.append({"LOG_POS", LOG_POS}); ret.append({"not_found_path", path}); response->setStatusCode(drogon::k404NotFound); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody(ret.to_json_string()); response->addHeader("Cache-Control", "no-store"); return response; } drogon::HttpResponsePtr return_tile_file( const drogon::HttpRequestPtr& req, const std::string& path) { auto info = get_file_cache_info(path); if (!info) { return make_tile_not_found_response(path); } const auto& if_none_match = req->getHeader("if-none-match"); bool not_modified; if (!if_none_match.empty()) { not_modified = etag_matches(if_none_match, info->etag); } else { const auto& if_modified_since = req->getHeader("if-modified-since"); not_modified = !if_modified_since.empty() && if_modified_since == info->last_modified; } if (not_modified) { auto response = drogon::HttpResponse::newHttpResponse(); response->setStatusCode(drogon::k304NotModified); response->setContentTypeCode(drogon::CT_NONE); add_tile_cache_headers(response, *info); return response; } auto content_type = httplib::detail::find_content_type( path, {}, "application/octet-stream"); auto response = drogon::HttpResponse::newFileResponse( path, "", drogon::CT_CUSTOM, content_type, req); if (response->statusCode() == drogon::k200OK || response->statusCode() == drogon::k206PartialContent) { add_tile_cache_headers(response, *info); } return response; } } void Global::init_tiles() { static auto& web_server = web_server_config; drogon::app().registerHandlerViaRegex( R"(^/tiles/.*$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { auto path = web_server.get_true_tiles() + req->getPath().substr(6); callback(return_tile_file(req, path)); }, {drogon::Get}); }