262 lines
6.5 KiB
C++
262 lines
6.5 KiB
C++
#pragma once
|
|
|
|
#include "parser.hpp"
|
|
#include "response.hpp"
|
|
#include "common.hpp"
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <chrono>
|
|
#include <shared_mutex>
|
|
#include <thread>
|
|
#include <atomic>
|
|
#include <zlib.h>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <sys/inotify.h>
|
|
#include <unistd.h>
|
|
|
|
using std::string_view;
|
|
using std::string;
|
|
namespace fs = std::filesystem;
|
|
|
|
class StaticFileHandler {
|
|
private:
|
|
string url_prefix_;
|
|
string root_path_;
|
|
std::unordered_map<string, std::pair<string, std::chrono::time_point<std::chrono::file_clock>>> cache_;
|
|
std::unordered_set<string> 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<bool> 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<Bytef*>(const_cast<char*>(data.data()));
|
|
zs.avail_in = data.size();
|
|
|
|
string compressed;
|
|
compressed.reserve(data.size() / 2);
|
|
|
|
char buffer[32768];
|
|
do {
|
|
zs.next_out = reinterpret_cast<Bytef*>(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<string>{}(res.body)) + "\"";
|
|
|
|
if (req.method == HttpMethod::HEAD) {
|
|
res.body.clear();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}; |