瓦片处理磁盘缓存
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
#include "Global.h"
|
||||
#include "Performance_Monitor.h"
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
/**
|
||||
* 瓦片文件缓存策略:
|
||||
* * 首次请求通过 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<File_Cache_Info> 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<std::uint64_t>(info.st_size);
|
||||
result.modified_time = info.st_mtime;
|
||||
result.etag = std::format(
|
||||
"W/\"{:x}-{:x}\"",
|
||||
static_cast<std::uint64_t>(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<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
auto path = web_server.get_true_tiles() + req->getPath().substr(6);
|
||||
callback(return_tile_file(req, path));
|
||||
},
|
||||
{drogon::Get});
|
||||
}
|
||||
Reference in New Issue
Block a user