This commit is contained in:
2026-07-27 15:17:13 +08:00
parent 8370ff9e79
commit c99b16d000
15 changed files with 1169 additions and 428 deletions
+134
View File
@@ -399,6 +399,135 @@ Psc::JSON make_map_view_response(Global* global) {
ret.append({"current_imagery_key", global->map_resources_config.current_imagery_key});
return ret;
}
bool is_multipart_request(const drogon::HttpRequestPtr& req) {
auto content_type = req->getHeader("content-type");
return content_type.find("multipart/form-data") != std::string::npos;
}
std::string normalize_upload_extension(std::string extension) {
std::ranges::transform(extension, extension.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
if (!extension.empty() && extension.front() != '.') {
extension.insert(extension.begin(), '.');
}
return extension;
}
std::filesystem::path web_model_directory(Global* global) {
return std::filesystem::path(global->web_server_config.get_true_webapp()) / "model";
}
std::filesystem::path source_model_directory(Global* global) {
return std::filesystem::path(global->web_server_config.get_true_webapp()).parent_path() / "model";
}
bool is_safe_aircraft_model_key(std::string_view key) {
return !key.empty() && std::ranges::all_of(key, [](unsigned char ch) {
return std::isalnum(ch) != 0 || ch == '_';
});
}
std::string model_file_name(std::string_view model_type, std::string_view aircraft_model_key) {
if (model_type == "aircraft") {
return "aircraft-custom.glb";
}
if (model_type == "aircraft_type") {
if (!is_safe_aircraft_model_key(aircraft_model_key)) {
throw std::invalid_argument("invalid aircraft_model_key");
}
auto normalized_key = std::string(aircraft_model_key);
std::ranges::replace(normalized_key, '_', '-');
return "aircraft-" + normalized_key + ".glb";
}
if (model_type == "base_station") {
return "base-station-custom.glb";
}
throw std::invalid_argument("invalid model_type");
}
void save_uploaded_model_file(const drogon::HttpFile& upload,
std::string_view model_type,
std::string_view aircraft_model_key,
Global* global) {
auto extension = normalize_upload_extension(std::string(upload.getFileExtension()));
if (extension != ".glb") {
throw std::invalid_argument("model file must be .glb");
}
if (model_type == "aircraft_type" && !global->map_model_config.aircraft_models.contains(std::string(aircraft_model_key))) {
throw std::invalid_argument("unknown aircraft_model_key");
}
auto upload_dir = std::filesystem::path(drogon::app().getUploadPath());
std::filesystem::create_directories(upload_dir);
auto temp_name = std::string(model_type) + "_" + std::string(aircraft_model_key) + "_model_upload.tmp";
if (upload.saveAs(temp_name) != 0) {
throw std::runtime_error("cannot save uploaded model");
}
auto temp_path = upload_dir / temp_name;
auto file_name = model_file_name(model_type, aircraft_model_key);
auto web_dir = web_model_directory(global);
auto source_dir = source_model_directory(global);
std::filesystem::create_directories(web_dir);
std::filesystem::create_directories(source_dir);
std::filesystem::copy_file(temp_path, web_dir / file_name, std::filesystem::copy_options::overwrite_existing);
std::filesystem::copy_file(temp_path, source_dir / file_name, std::filesystem::copy_options::overwrite_existing);
std::filesystem::remove(temp_path);
auto url = "/ui/model/" + file_name;
if (model_type == "aircraft") {
global->map_model_config.aircraft_model.url = url;
}
else if (model_type == "aircraft_type") {
global->map_model_config.aircraft_models[std::string(aircraft_model_key)].url = url;
}
else {
global->map_model_config.base_station_model.url = url;
}
}
void handle_map_model_upload(const drogon::HttpRequestPtr& req, Global* global) {
drogon::MultiPartParser parser;
if (parser.parse(req) != 0) {
throw std::invalid_argument("invalid multipart model upload");
}
auto model_type = parser.getOptionalParameter<std::string>("model_type");
if (!model_type || parser.getFiles().size() != 1) {
throw std::invalid_argument("model_type or file");
}
auto aircraft_model_key = parser.getOptionalParameter<std::string>("aircraft_model_key").value_or("");
save_uploaded_model_file(parser.getFiles().front(), *model_type, aircraft_model_key, global);
Global::save();
}
void handle_map_model_config_update(const drogon::HttpRequestPtr& req, Global* global) {
auto parsed = Psc::try_parse_json(req->body().data());
if (!parsed.has_value()) {
throw std::invalid_argument("invalid map_models");
}
global->map_model_config.from_base_json(&parsed.value());
Global::save();
}
void register_map_models_handler() {
drogon::app().registerHandlerViaRegex(
R"(^/map/models$)",
[](const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
auto global = Global::instance();
auto response = drogon::HttpResponse::newHttpResponse();
try {
if (req->method() == drogon::Post) {
if (is_multipart_request(req)) {
handle_map_model_upload(req, global);
}
else {
handle_map_model_config_update(req, global);
}
}
response->setStatusCode(drogon::k200OK);
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
response->setBody(global->map_model_config.to_base_json().to_json_string());
}
catch (const std::exception& error) {
response->setStatusCode(drogon::k400BadRequest);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody(Psc::platform_2_utf8(error.what()));
}
response->addHeader("Cache-Control", "no-store");
callback(response);
},
{drogon::Get, drogon::Post});
}
void register_map_view_handler() {
drogon::app().registerHandlerViaRegex(
R"(^/map/view$)",
@@ -426,6 +555,10 @@ void register_map_view_handler() {
try {
auto next_view = global->map_view_config;
next_view.from_base_json(&parsed.value());
if (!global->map_resources_config.is_imagery_key(next_view.map2d.current_imagery_key) ||
!global->map_resources_config.is_imagery_key(next_view.map3d.current_imagery_key)) {
throw std::invalid_argument("invalid current_imagery_key");
}
global->map_resources_config.current_imagery_key = key.value();
global->map_view_config = next_view;
Global::save();
@@ -453,6 +586,7 @@ void Global::init_tiles() {
auto terrain = std::make_shared<Tile_Source>(map_resources_config.terrain);
register_map_resources_handler();
register_map_graphics_handler();
register_map_models_handler();
register_map_view_handler();
register_tile_handler(R"(^/map/imagery/.*$)", "/map/imagery/", imagery);
register_tile_handler(R"(^/tiles/.*$)", "/tiles/", google_imagery);