#pragma once #include "parser.hpp" #include "response.hpp" #include "common.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::string_view; using std::string; namespace fs = std::filesystem; class StaticFileHandler { private: string url_prefix_; string root_path_; std::unordered_map>> cache_; std::unordered_set file_index_; mutable std::shared_mutex index_mutex_; size_t max_cache_size_ = 50 * 1024 * 1024; // 50MB cache limit int inotify_fd_ = -1; int watch_fd_ = -1; std::thread watch_thread_; std::atomic stop_watching_{false}; void build_file_index() { std::unique_lock lock(index_mutex_); file_index_.clear(); if (!fs::exists(root_path_) || !fs::is_directory(root_path_)) { return; } for (const auto& entry : fs::recursive_directory_iterator(root_path_)) { if (entry.is_regular_file()) { auto rel_path = fs::relative(entry.path(), root_path_); file_index_.insert(rel_path.string()); } } } void invalidate_cache() { std::unique_lock lock(index_mutex_); cache_.clear(); } void watch_files() { char buffer[4096]; while (!stop_watching_) { int len = read(inotify_fd_, buffer, sizeof(buffer)); if (len <= 0) continue; bool needs_rebuild = false; for (int i = 0; i < len;) { struct inotify_event* event = (struct inotify_event*)&buffer[i]; if (event->mask & (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO)) { needs_rebuild = true; break; } i += sizeof(struct inotify_event) + event->len; } if (needs_rebuild) { build_file_index(); invalidate_cache(); } } } bool file_exists_fast(const string& relative_path) const { std::shared_lock lock(index_mutex_); return file_index_.count(relative_path) > 0; } string compress_gzip(const string& data) const { z_stream zs; memset(&zs, 0, sizeof(zs)); if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) { return ""; } zs.next_in = reinterpret_cast(const_cast(data.data())); zs.avail_in = data.size(); string compressed; compressed.reserve(data.size() / 2); char buffer[32768]; do { zs.next_out = reinterpret_cast(buffer); zs.avail_out = sizeof(buffer); int ret = deflate(&zs, Z_FINISH); if (ret == Z_STREAM_ERROR) break; size_t have = sizeof(buffer) - zs.avail_out; compressed.append(buffer, have); } while (zs.avail_out == 0); deflateEnd(&zs); return compressed; } string read_file(const string& path) const { std::ifstream file(path, std::ios::binary); if (!file.is_open()) return ""; file.seekg(0, std::ios::end); size_t size = file.tellg(); file.seekg(0, std::ios::beg); string content(size, '\0'); file.read(content.data(), size); return content; } bool is_path_safe(string_view path) const { return path.find("..") == string::npos; } public: explicit StaticFileHandler(string_view root_path, string_view url_prefix = "") : url_prefix_(url_prefix), root_path_(root_path) { if (!root_path_.empty() && root_path_.back() != '/') { root_path_ += '/'; } if (!url_prefix_.empty() && url_prefix_.back() != '/') { url_prefix_ += '/'; } build_file_index(); start_watching(); } ~StaticFileHandler() { stop_watching_ = true; if (watch_thread_.joinable()) { watch_thread_.join(); } if (watch_fd_ != -1) close(watch_fd_); if (inotify_fd_ != -1) close(inotify_fd_); } void start_watching() { inotify_fd_ = inotify_init1(IN_NONBLOCK); if (inotify_fd_ == -1) return; watch_fd_ = inotify_add_watch(inotify_fd_, root_path_.c_str(), IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO); if (watch_fd_ == -1) { close(inotify_fd_); inotify_fd_ = -1; return; } watch_thread_ = std::thread(&StaticFileHandler::watch_files, this); } void refresh_index() { build_file_index(); } bool handle(const Request& req, Response& res) { if (req.method != HttpMethod::GET && req.method != HttpMethod::HEAD) { return false; } // Check URL prefix match if (!url_prefix_.empty()) { if (req.path.size() < url_prefix_.size() || req.path.substr(0, url_prefix_.size()) != url_prefix_) { return false; } } // Get path after prefix string_view file_path_part = url_prefix_.empty() ? req.path : req.path.substr(url_prefix_.size()); if (!is_path_safe(file_path_part)) { return false; } string relative_path; if (file_path_part.empty() || file_path_part == "/") { relative_path = "index.html"; } else { // Remove leading slash if present if (file_path_part[0] == '/') file_path_part = file_path_part.substr(1); relative_path = string(file_path_part); } // Fast file existence check if (!file_exists_fast(relative_path)) { return false; } string file_path = root_path_ + relative_path; // Get file info auto last_write = fs::last_write_time(file_path); auto file_size = fs::file_size(file_path); // Check cache auto cache_it = cache_.find(file_path); if (cache_it != cache_.end() && cache_it->second.second == last_write) { res.body = cache_it->second.first; } else { // Read file res.body = read_file(file_path); if (res.body.empty()) { return false; } // Cache if small enough if (file_size < max_cache_size_ / 10) { cache_[file_path] = {res.body, last_write}; } } // Set MIME type string ext = fs::path(file_path).extension(); res.content_type = MimeTypes::get_mime_type(ext); // Check if client accepts gzip auto accept_encoding = req.headers.find("Accept-Encoding"); bool client_accepts_gzip = accept_encoding != req.headers.end() && accept_encoding->second.find("gzip") != string::npos; // Compress if appropriate if (client_accepts_gzip && MimeTypes::should_compress(res.content_type) && res.body.size() > 1024) { string compressed = compress_gzip(res.body); if (!compressed.empty() && compressed.size() < res.body.size()) { res.body = std::move(compressed); res.headers["Content-Encoding"] = "gzip"; } } // Add caching headers res.headers["Cache-Control"] = "public, max-age=3600"; res.headers["ETag"] = "\"" + std::to_string(std::hash{}(res.body)) + "\""; if (req.method == HttpMethod::HEAD) { res.body.clear(); } return true; } };