Clean up static assets
This file has several long lambdas, and some functions that should be
broken down into smaller components. This is a refactor that should
make no functional changes.
Tested: Webui-vue loads correctly, shows static files are working as
designed.
Change-Id: Ie272a15fd935823743303c287a475114a734feb5
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/include/webassets.hpp b/include/webassets.hpp
index 4bcc8cb..9ac2abd 100644
--- a/include/webassets.hpp
+++ b/include/webassets.hpp
@@ -8,23 +8,18 @@
#include <boost/container/flat_set.hpp>
+#include <algorithm>
+#include <array>
#include <filesystem>
#include <fstream>
#include <string>
+#include <string_view>
namespace crow
{
namespace webassets
{
-struct CmpStr
-{
- bool operator()(const char* a, const char* b) const
- {
- return std::strcmp(a, b) < 0;
- }
-};
-
inline std::string getStaticEtag(const std::filesystem::path& webpath)
{
// webpack outputs production chunks in the form:
@@ -56,40 +51,169 @@
return std::format("\"{}\"", hash);
}
-inline void requestRoutes(App& app)
+static constexpr std::string_view rootpath("/usr/share/www/");
+
+struct StaticFile
{
- constexpr static std::array<std::pair<const char*, const char*>, 17>
+ std::filesystem::path absolutePath;
+ std::string_view contentType;
+ std::string_view contentEncoding;
+ std::string etag;
+ bool renamed = false;
+};
+
+inline void
+ handleStaticAsset(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const StaticFile& file)
+{
+ if (!file.contentType.empty())
+ {
+ asyncResp->res.addHeader(boost::beast::http::field::content_type,
+ file.contentType);
+ }
+
+ if (!file.contentEncoding.empty())
+ {
+ asyncResp->res.addHeader(boost::beast::http::field::content_encoding,
+ file.contentEncoding);
+ }
+
+ if (!file.etag.empty())
+ {
+ asyncResp->res.addHeader(boost::beast::http::field::etag, file.etag);
+ // Don't cache paths that don't have the etag in them, like
+ // index, which gets transformed to /
+ if (!file.renamed)
+ {
+ // Anything with a hash can be cached forever and is
+ // immutable
+ asyncResp->res.addHeader(boost::beast::http::field::cache_control,
+ "max-age=31556926, immutable");
+ }
+
+ std::string_view cachedEtag =
+ req.getHeaderValue(boost::beast::http::field::if_none_match);
+ if (cachedEtag == file.etag)
+ {
+ asyncResp->res.result(boost::beast::http::status::not_modified);
+ return;
+ }
+ }
+
+ if (!asyncResp->res.openFile(file.absolutePath))
+ {
+ BMCWEB_LOG_DEBUG("failed to read file");
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ return;
+ }
+}
+
+inline std::string_view getFiletypeForExtension(std::string_view extension)
+{
+ constexpr static std::array<std::pair<std::string_view, std::string_view>,
+ 17>
contentTypes{
{{".css", "text/css;charset=UTF-8"},
- {".html", "text/html;charset=UTF-8"},
- {".js", "application/javascript;charset=UTF-8"},
- {".png", "image/png;charset=UTF-8"},
- {".woff", "application/x-font-woff"},
- {".woff2", "application/x-font-woff2"},
- {".gif", "image/gif"},
- {".ico", "image/x-icon"},
- {".ttf", "application/x-font-ttf"},
- {".svg", "image/svg+xml"},
{".eot", "application/vnd.ms-fontobject"},
- {".xml", "application/xml"},
- {".json", "application/json"},
- {".jpg", "image/jpeg"},
+ {".gif", "image/gif"},
+ {".html", "text/html;charset=UTF-8"},
+ {".ico", "image/x-icon"},
{".jpeg", "image/jpeg"},
+ {".jpg", "image/jpeg"},
+ {".js", "application/javascript;charset=UTF-8"},
+ {".json", "application/json"},
// dev tools don't care about map type, setting to json causes
// browser to show as text
// https://stackoverflow.com/questions/19911929/what-mime-type-should-i-use-for-javascript-source-map-files
- {".map", "application/json"}}};
+ {".map", "application/json"},
+ {".png", "image/png;charset=UTF-8"},
+ {".svg", "image/svg+xml"},
+ {".ttf", "application/x-font-ttf"},
+ {".woff", "application/x-font-woff"},
+ {".woff2", "application/x-font-woff2"},
+ {".xml", "application/xml"}}};
- std::filesystem::path rootpath{"/usr/share/www/"};
+ const auto* contentType = std::ranges::find_if(
+ contentTypes,
+ [&extension](const auto& val) { return val.first == extension; });
+ if (contentType == contentTypes.end())
+ {
+ BMCWEB_LOG_ERROR(
+ "Cannot determine content-type for file with extension {}",
+ extension);
+ return "";
+ }
+ return contentType->second;
+}
+
+inline void addFile(App& app, const std::filesystem::directory_entry& dir)
+{
+ StaticFile file;
+ file.absolutePath = dir.path();
+ std::filesystem::path relativePath(
+ file.absolutePath.string().substr(rootpath.size() - 1));
+
+ std::string extension = relativePath.extension();
+ std::filesystem::path webpath = relativePath;
+
+ if (extension == ".gz")
+ {
+ webpath = webpath.replace_extension("");
+ // Use the non-gzip version for determining content type
+ extension = webpath.extension().string();
+ file.contentEncoding = "gzip";
+ }
+
+ file.etag = getStaticEtag(webpath);
+
+ if (webpath.filename().string().starts_with("index."))
+ {
+ webpath = webpath.parent_path();
+ if (webpath.string().empty() || webpath.string().back() != '/')
+ {
+ // insert the non-directory version of this path
+ webroutes::routes.insert(webpath);
+ webpath += "/";
+ file.renamed = true;
+ }
+ }
+
+ std::pair<boost::container::flat_set<std::string>::iterator, bool>
+ inserted = webroutes::routes.insert(webpath);
+
+ if (!inserted.second)
+ {
+ // Got a duplicated path. This is expected in certain
+ // situations
+ BMCWEB_LOG_DEBUG("Got duplicated path {}", webpath.string());
+ return;
+ }
+ file.contentType = getFiletypeForExtension(extension);
+
+ if (webpath == "/")
+ {
+ forward_unauthorized::hasWebuiRoute = true;
+ }
+
+ app.routeDynamic(webpath)(
+ [file = std::move(file)](
+ const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ handleStaticAsset(req, asyncResp, file);
+ });
+}
+
+inline void requestRoutes(App& app)
+{
std::error_code ec;
-
- std::filesystem::recursive_directory_iterator dirIter(rootpath, ec);
+ std::filesystem::recursive_directory_iterator dirIter({rootpath}, ec);
if (ec)
{
BMCWEB_LOG_ERROR(
- "Unable to find or open {} static file hosting disabled",
- rootpath.string());
+ "Unable to find or open {} static file hosting disabled", rootpath);
return;
}
@@ -104,9 +228,6 @@
for (const std::filesystem::directory_entry& dir : paths)
{
- const std::filesystem::path& absolutePath = dir.path();
- std::filesystem::path relativePath{
- absolutePath.string().substr(rootpath.string().size() - 1)};
if (std::filesystem::is_directory(dir))
{
// don't recurse into hidden directories or symlinks
@@ -118,120 +239,7 @@
}
else if (std::filesystem::is_regular_file(dir))
{
- std::string extension = relativePath.extension();
- std::filesystem::path webpath = relativePath;
- const char* contentEncoding = nullptr;
-
- if (extension == ".gz")
- {
- webpath = webpath.replace_extension("");
- // Use the non-gzip version for determining content type
- extension = webpath.extension().string();
- contentEncoding = "gzip";
- }
-
- std::string etag = getStaticEtag(webpath);
-
- bool renamed = false;
- if (webpath.filename().string().starts_with("index."))
- {
- webpath = webpath.parent_path();
- if (webpath.string().empty() || webpath.string().back() != '/')
- {
- // insert the non-directory version of this path
- webroutes::routes.insert(webpath);
- webpath += "/";
- renamed = true;
- }
- }
-
- std::pair<boost::container::flat_set<std::string>::iterator, bool>
- inserted = webroutes::routes.insert(webpath);
-
- if (!inserted.second)
- {
- // Got a duplicated path. This is expected in certain
- // situations
- BMCWEB_LOG_DEBUG("Got duplicated path {}", webpath.string());
- continue;
- }
- const char* contentType = nullptr;
-
- for (const std::pair<const char*, const char*>& ext : contentTypes)
- {
- if (ext.first == nullptr || ext.second == nullptr)
- {
- continue;
- }
- if (extension == ext.first)
- {
- contentType = ext.second;
- }
- }
-
- if (contentType == nullptr)
- {
- BMCWEB_LOG_ERROR(
- "Cannot determine content-type for {} with extension {}",
- absolutePath.string(), extension);
- }
-
- if (webpath == "/")
- {
- forward_unauthorized::hasWebuiRoute = true;
- }
-
- app.routeDynamic(webpath)(
- [absolutePath, contentType, contentEncoding, etag,
- renamed](const crow::Request& req,
- const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
- if (contentType != nullptr)
- {
- asyncResp->res.addHeader(
- boost::beast::http::field::content_type, contentType);
- }
-
- if (contentEncoding != nullptr)
- {
- asyncResp->res.addHeader(
- boost::beast::http::field::content_encoding,
- contentEncoding);
- }
-
- if (!etag.empty())
- {
- asyncResp->res.addHeader(boost::beast::http::field::etag,
- etag);
- // Don't cache paths that don't have the etag in them, like
- // index, which gets transformed to /
- if (!renamed)
- {
- // Anything with a hash can be cached forever and is
- // immutable
- asyncResp->res.addHeader(
- boost::beast::http::field::cache_control,
- "max-age=31556926, immutable");
- }
-
- std::string_view cachedEtag = req.getHeaderValue(
- boost::beast::http::field::if_none_match);
- if (cachedEtag == etag)
- {
- asyncResp->res.result(
- boost::beast::http::status::not_modified);
- return;
- }
- }
-
- // res.set_header("Cache-Control", "public, max-age=86400");
- if (!asyncResp->res.openFile(absolutePath))
- {
- BMCWEB_LOG_DEBUG("failed to read file");
- asyncResp->res.result(
- boost::beast::http::status::internal_server_error);
- return;
- }
- });
+ addFile(app, dir);
}
}
}