66#include < chrono>
77#include < cstring>
88#include < filesystem>
9+ #include < fstream>
910#include < httplib.h>
1011#include < atomic>
1112#include < cctype>
13+ #include < unordered_map>
1214#include < mutex>
1315#include < nlohmann/json.hpp>
1416
@@ -20,6 +22,64 @@ std::string make_error_json(const std::string& message) {
2022 return nlohmann::json{{" error" , message}}.dump ();
2123}
2224
25+ std::string get_mime_type_for_path (const std::filesystem::path& path) {
26+ static const std::unordered_map<std::string, std::string> kMimeByExtension {
27+ {" .css" , " text/css" },
28+ {" .csv" , " text/csv" },
29+ {" .gif" , " image/gif" },
30+ {" .htm" , " text/html" },
31+ {" .html" , " text/html" },
32+ {" .ico" , " image/x-icon" },
33+ {" .jpeg" , " image/jpeg" },
34+ {" .jpg" , " image/jpeg" },
35+ {" .js" , " application/javascript" },
36+ {" .json" , " application/json" },
37+ {" .map" , " application/json" },
38+ {" .mjs" , " application/javascript" },
39+ {" .png" , " image/png" },
40+ {" .svg" , " image/svg+xml" },
41+ {" .txt" , " text/plain" },
42+ {" .wasm" , " application/wasm" },
43+ {" .webp" , " image/webp" },
44+ {" .woff" , " font/woff" },
45+ {" .woff2" , " font/woff2" },
46+ {" .xml" , " application/xml" },
47+ };
48+
49+ const auto ext = path.extension ().string ();
50+ const auto it = kMimeByExtension .find (ext);
51+ if (it != kMimeByExtension .end ()) {
52+ return it->second ;
53+ }
54+ return " application/octet-stream" ;
55+ }
56+
57+ bool read_file (const std::filesystem::path& path, std::string& output) {
58+ std::ifstream file (path, std::ios::binary);
59+ if (!file.is_open ()) {
60+ return false ;
61+ }
62+ output.assign (std::istreambuf_iterator<char >(file), std::istreambuf_iterator<char >());
63+ return !file.bad ();
64+ }
65+
66+ bool serve_file_response (httplib::Response& res,
67+ const std::filesystem::path& path,
68+ const std::filesystem::path& mime_from_path,
69+ bool gzip_encoded) {
70+ std::string body;
71+ if (!read_file (path, body)) {
72+ return false ;
73+ }
74+
75+ res.set_content (body, get_mime_type_for_path (mime_from_path));
76+ if (gzip_encoded) {
77+ res.set_header (" Content-Encoding" , " gzip" );
78+ res.set_header (" Vary" , " Accept-Encoding" );
79+ }
80+ return true ;
81+ }
82+
2383} // namespace
2484
2585struct ApiServer ::Impl {
@@ -162,16 +222,47 @@ bool ApiServer::register_static_root(const std::string& frontend_root) {
162222 }
163223
164224 fs::path requested = root / relative;
225+ fs::path requested_gzip = requested;
226+ requested_gzip += " .gz" ;
227+
228+ if (fs::is_regular_file (requested_gzip)) {
229+ if (serve_file_response (res, requested_gzip, requested, true )) {
230+ return ;
231+ }
232+ res.status = 500 ;
233+ res.set_content (make_error_json (" failed to read static file" ), " application/json" );
234+ return ;
235+ }
236+
165237 if (fs::is_regular_file (requested)) {
166- res.set_file_content (requested.string ());
238+ if (serve_file_response (res, requested, requested, false )) {
239+ return ;
240+ }
241+ res.status = 500 ;
242+ res.set_content (make_error_json (" failed to read static file" ), " application/json" );
167243 return ;
168244 }
169245
170246 if (!fs::is_regular_file (index_path)) {
171247 res.status = 404 ;
172248 } else {
173- res.set_file_content (index_path.string ());
174- res.status = 200 ;
249+ fs::path index_gzip = index_path;
250+ index_gzip += " .gz" ;
251+ if (fs::is_regular_file (index_gzip)) {
252+ if (serve_file_response (res, index_gzip, index_path, true )) {
253+ res.status = 200 ;
254+ return ;
255+ }
256+ res.status = 500 ;
257+ res.set_content (make_error_json (" failed to read static file" ), " application/json" );
258+ return ;
259+ }
260+ if (serve_file_response (res, index_path, index_path, false )) {
261+ res.status = 200 ;
262+ } else {
263+ res.status = 500 ;
264+ res.set_content (make_error_json (" failed to read static file" ), " application/json" );
265+ }
175266 }
176267 });
177268
0 commit comments