Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/vars.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ set(HTTP_SERVER_HEADERS
http/server/HttpContext.h
http/server/HttpResponseWriter.h
http/server/WebSocketServer.h
http/server/FileCache.h
)

set(MQTT_HEADERS
Expand Down
137 changes: 87 additions & 50 deletions http/server/FileCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

#define ETAG_FMT "\"%zx-%zx\""

FileCache::FileCache(size_t capacity) : hv::LRUCache<std::string, file_cache_ptr>(capacity) {
stat_interval = 10; // s
expired_time = 60; // s
FileCache::FileCache(size_t capacity)
: hv::LRUCache<std::string, file_cache_ptr>(capacity) {
stat_interval = 10; // s
expired_time = 60; // s
max_header_length = FILE_CACHE_DEFAULT_HEADER_LENGTH;
max_file_size = FILE_CACHE_DEFAULT_MAX_FILE_SIZE;
}

file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
Expand All @@ -26,6 +29,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
#endif
bool modified = false;
if (fc) {
std::lock_guard<std::mutex> lock(fc->mutex);
time_t now = time(NULL);
if (now - fc->stat_time > stat_interval) {
fc->stat_time = now;
Expand All @@ -52,85 +56,112 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) {
flags |= O_BINARY;
#endif
int fd = -1;
bool is_dir = false;
#ifdef OS_WIN
if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath);
if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) {
if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath);
if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) {
param->error = ERR_OPEN_FILE;
return NULL;
}
if(S_ISREG(st.st_mode)) {
if (S_ISREG(st.st_mode)) {
fd = _wopen(wfilepath.c_str(), flags);
}else if (S_ISDIR(st.st_mode)) {
// NOTE: open(dir) return -1 on windows
fd = 0;
} else if (S_ISDIR(st.st_mode)) {
is_dir = true;
}
#else
if(stat(filepath, &st) != 0) {
if (::stat(filepath, &st) != 0) {
param->error = ERR_OPEN_FILE;
return NULL;
}
fd = open(filepath, flags);
#endif
if (fd < 0) {
if (fd < 0 && !is_dir) {
param->error = ERR_OPEN_FILE;
return NULL;
}
defer(if (fd > 0) { close(fd); })
defer(if (fd >= 0) { close(fd); })
if (fc == NULL) {
if (S_ISREG(st.st_mode) ||
(S_ISDIR(st.st_mode) &&
filepath[strlen(filepath)-1] == '/')) {
filepath[strlen(filepath) - 1] == '/')) {
fc = std::make_shared<file_cache_t>();
fc->filepath = filepath;
fc->st = st;
fc->header_reserve = max_header_length;
time(&fc->open_time);
fc->stat_time = fc->open_time;
fc->stat_cnt = 1;
put(filepath, fc);
}
else {
// NOTE: do NOT put() into cache yet — defer until fully initialized
} else {
param->error = ERR_MISMATCH;
return NULL;
}
}
if (S_ISREG(fc->st.st_mode)) {
param->filesize = fc->st.st_size;
// FILE
if (param->need_read) {
if (fc->st.st_size > param->max_read) {
param->error = ERR_OVER_LIMIT;
return NULL;
// Hold fc->mutex for initialization, but release before put()
// to avoid lock-order inversion with RemoveExpiredFileCache().
// Lock order: LRUCache mutex → fc->mutex (never reverse).
{
std::lock_guard<std::mutex> lock(fc->mutex);
if (S_ISREG(st.st_mode)) {
param->filesize = st.st_size;
// FILE
if (param->need_read) {
if (st.st_size > param->max_read) {
param->error = ERR_OVER_LIMIT;
// Leave existing cache entry's state untouched
return NULL;
}
}
fc->resize_buf(fc->st.st_size);
int nread = read(fd, fc->filebuf.base, fc->filebuf.len);
if (nread != fc->filebuf.len) {
hloge("Failed to read file: %s", filepath);
param->error = ERR_READ_FILE;
return NULL;
// Validation passed — commit new stat into cached entry
fc->st = st;
if (param->need_read) {
fc->resize_buf(fc->st.st_size, max_header_length);
// Loop to handle partial reads (EINTR, etc.)
char* dst = fc->filebuf.base;
size_t remaining = fc->filebuf.len;
while (remaining > 0) {
ssize_t nread = read(fd, dst, remaining);
if (nread < 0) {
if (errno == EINTR) continue;
hloge("Failed to read file: %s", filepath);
param->error = ERR_READ_FILE;
return NULL;
}
if (nread == 0) {
hloge("Unexpected EOF reading file: %s", filepath);
param->error = ERR_READ_FILE;
return NULL;
}
dst += nread;
remaining -= nread;
}
}
}
const char* suffix = strrchr(filepath, '.');
if (suffix) {
http_content_type content_type = http_content_type_enum_by_suffix(suffix+1);
if (content_type == TEXT_HTML) {
fc->content_type = "text/html; charset=utf-8";
} else if (content_type == TEXT_PLAIN) {
fc->content_type = "text/plain; charset=utf-8";
} else {
fc->content_type = http_content_type_str_by_suffix(suffix+1);
const char* suffix = strrchr(filepath, '.');
if (suffix) {
http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1);
if (content_type == TEXT_HTML) {
fc->content_type = "text/html; charset=utf-8";
} else if (content_type == TEXT_PLAIN) {
fc->content_type = "text/plain; charset=utf-8";
} else {
fc->content_type = http_content_type_str_by_suffix(suffix + 1);
}
}
} else if (S_ISDIR(st.st_mode)) {
// DIR
fc->st = st;
std::string page;
make_index_of_page(filepath, page, param->path);
fc->resize_buf(page.size(), max_header_length);
memcpy(fc->filebuf.base, page.c_str(), page.size());
fc->content_type = "text/html; charset=utf-8";
}
}
else if (S_ISDIR(fc->st.st_mode)) {
// DIR
std::string page;
make_index_of_page(filepath, page, param->path);
fc->resize_buf(page.size());
memcpy(fc->filebuf.base, page.c_str(), page.size());
fc->content_type = "text/html; charset=utf-8";
}
gmtime_fmt(fc->st.st_mtime, fc->last_modified);
snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, (size_t)fc->st.st_mtime, (size_t)fc->st.st_size);
gmtime_fmt(fc->st.st_mtime, fc->last_modified);
snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT,
(size_t)fc->st.st_mtime, (size_t)fc->st.st_size);
} // release fc->mutex before put() to maintain lock ordering
// Cache the fully initialized entry (acquires LRUCache mutex only)
put(filepath, fc);
}
return fc;
}
Expand All @@ -154,6 +185,12 @@ file_cache_ptr FileCache::Get(const char* filepath) {
void FileCache::RemoveExpiredFileCache() {
time_t now = time(NULL);
remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) {
// Use try_to_lock to avoid lock-order inversion with Open().
// If the entry is busy, skip it — it will be checked next cycle.
std::unique_lock<std::mutex> lock(fc->mutex, std::try_to_lock);
if (!lock.owns_lock()) {
return false;
}
return (now - fc->stat_time > expired_time);
});
}
122 changes: 95 additions & 27 deletions http/server/FileCache.h
Original file line number Diff line number Diff line change
@@ -1,90 +1,158 @@
#ifndef HV_FILE_CACHE_H_
#define HV_FILE_CACHE_H_

/*
* FileCache — Enhanced File Cache for libhv HTTP server
*
* Features:
* 1. Configurable max_header_length (default 4096, tunable per-instance)
* 2. prepend_header() returns bool to report success/failure
* 3. Exposes header/buffer metrics via accessors
* 4. Fixes stat() name collision in is_modified()
* 5. max_cache_num / max_file_size configurable at runtime
* 6. Reserved header space can be tuned per-instance
* 7. Source-level API compatible; struct layout differs from original (no ABI/layout compatibility)
*/

#include <memory>
#include <map>
#include <string>
#include <mutex>

#include "hexport.h"
#include "hbuf.h"
#include "hstring.h"
#include "LRUCache.h"

#define HTTP_HEADER_MAX_LENGTH 1024 // 1K
#define FILE_CACHE_MAX_NUM 100
#define FILE_CACHE_MAX_SIZE (1 << 22) // 4M
// Default values — may be overridden at runtime via FileCache setters
#define FILE_CACHE_DEFAULT_HEADER_LENGTH 4096 // 4K
#define FILE_CACHE_DEFAULT_MAX_NUM 100
#define FILE_CACHE_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M

typedef struct file_cache_s {
mutable std::mutex mutex; // protects all mutable state below
std::string filepath;
struct stat st;
time_t open_time;
time_t stat_time;
uint32_t stat_cnt;
HBuf buf; // http_header + file_content
hbuf_t filebuf;
hbuf_t httpbuf;
HBuf buf; // header_reserve + file_content
hbuf_t filebuf; // points into buf: file content region
hbuf_t httpbuf; // points into buf: header + file content after prepend
char last_modified[64];
char etag[64];
std::string content_type;

// --- new: expose header metrics ---
int header_reserve; // reserved bytes before file content
int header_used; // actual bytes used by prepend_header

file_cache_s() {
stat_cnt = 0;
header_reserve = FILE_CACHE_DEFAULT_HEADER_LENGTH;
header_used = 0;
memset(last_modified, 0, sizeof(last_modified));
memset(etag, 0, sizeof(etag));
}

// NOTE: caller must hold mutex.
// On Windows, Open() uses _wstat() directly instead of calling this.
bool is_modified() {
time_t mtime = st.st_mtime;
stat(filepath.c_str(), &st);
::stat(filepath.c_str(), &st);
return mtime != st.st_mtime;
}

// NOTE: caller must hold mutex
bool is_complete() {
if(S_ISDIR(st.st_mode)) return filebuf.len > 0;
return filebuf.len == st.st_size;
if (S_ISDIR(st.st_mode)) return filebuf.len > 0;
return filebuf.len == (size_t)st.st_size;
}

void resize_buf(int filesize) {
buf.resize(HTTP_HEADER_MAX_LENGTH + filesize);
filebuf.base = buf.base + HTTP_HEADER_MAX_LENGTH;
// NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers
void resize_buf(size_t filesize, int reserved) {
if (reserved < 0) reserved = 0;
header_reserve = reserved;
buf.resize((size_t)reserved + filesize);
filebuf.base = buf.base + reserved;
filebuf.len = filesize;
// Invalidate httpbuf since buffer may have been reallocated
httpbuf.base = NULL;
httpbuf.len = 0;
header_used = 0;
}

void prepend_header(const char* header, int len) {
if (len > HTTP_HEADER_MAX_LENGTH) return;
void resize_buf(size_t filesize) {
resize_buf(filesize, header_reserve);
}

// Thread-safe: prepend header into reserved space.
// Returns true on success, false if header exceeds reserved space.
// On failure, httpbuf falls back to filebuf (body only, no header).
bool prepend_header(const char* header, int len) {
std::lock_guard<std::mutex> lock(mutex);
if (len <= 0 || len > header_reserve) {
// Safe fallback: point httpbuf at filebuf so callers always get valid data
httpbuf = filebuf;
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On prepend_header() failure (len <= 0 || len > header_reserve), header_used is not reset. If a previous call succeeded, metrics returned by get_header_used() / get_header_remaining() will be stale/misleading after a failed prepend. Consider setting header_used = 0 in the failure path as well.

Suggested change
httpbuf = filebuf;
httpbuf = filebuf;
header_used = 0;

Copilot uses AI. Check for mistakes.
header_used = 0;
return false;
}
httpbuf.base = filebuf.base - len;
httpbuf.len = len + filebuf.len;
httpbuf.len = (size_t)len + filebuf.len;
memcpy(httpbuf.base, header, len);
header_used = len;
return true;
}

// --- thread-safe accessors ---
int get_header_reserve() const { std::lock_guard<std::mutex> lock(mutex); return header_reserve; }
int get_header_used() const { std::lock_guard<std::mutex> lock(mutex); return header_used; }
int get_header_remaining() const { std::lock_guard<std::mutex> lock(mutex); return header_reserve - header_used; }
bool header_fits(int len) const { std::lock_guard<std::mutex> lock(mutex); return len > 0 && len <= header_reserve; }
} file_cache_t;

typedef std::shared_ptr<file_cache_t> file_cache_ptr;
typedef std::shared_ptr<file_cache_t> file_cache_ptr;

class FileCache : public hv::LRUCache<std::string, file_cache_ptr> {
class HV_EXPORT FileCache : public hv::LRUCache<std::string, file_cache_ptr> {
public:
int stat_interval;
int expired_time;
// --- configurable parameters (were hardcoded macros before) ---
int stat_interval; // seconds between stat() checks
int expired_time; // seconds before cache entry expires
int max_header_length; // reserved header bytes per entry
int max_file_size; // max cached file size (larger = large-file path)

FileCache(size_t capacity = FILE_CACHE_MAX_NUM);
explicit FileCache(size_t capacity = FILE_CACHE_DEFAULT_MAX_NUM);

struct OpenParam {
bool need_read;
int max_read;
const char* path;
size_t filesize;
int error;
bool need_read;
int max_read; // per-request override for max file size
const char* path; // URL path (for directory listing)
size_t filesize; // [out] actual file size
int error; // [out] error code if Open returns NULL
Comment on lines +117 to +130
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_file_size / OpenParam::max_read are declared as int, but they represent a byte size and are compared against st.st_size (typically off_t, potentially >2GB). Using int can overflow/truncate on large files and makes it harder to configure sizes beyond INT_MAX. Consider switching these fields and related APIs to size_t (or uint64_t) so large-file thresholds work correctly on 64-bit platforms.

Copilot uses AI. Check for mistakes.

OpenParam() {
need_read = true;
max_read = FILE_CACHE_MAX_SIZE;
max_read = FILE_CACHE_DEFAULT_MAX_FILE_SIZE;
path = "/";
filesize = 0;
error = 0;
}
Comment on lines 125 to 138
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenParam::max_read defaults to FILE_CACHE_DEFAULT_MAX_FILE_SIZE, while FileCache now has a runtime-configurable max_file_size (and HttpServer sets it from service->max_file_cache_size). Callers that don't explicitly set param.max_read will silently ignore the instance’s configured max_file_size, which can lead to inconsistent caching behavior across call sites. Consider defaulting OpenParam::max_read from the owning FileCache instance (e.g., in Open() when param->max_read is unset/0), or remove one of these knobs to keep a single source of truth.

Copilot uses AI. Check for mistakes.
};

file_cache_ptr Open(const char* filepath, OpenParam* param);
bool Exists(const char* filepath) const;
bool Close(const char* filepath);
void RemoveExpiredFileCache();

// --- new: getters ---
int GetMaxHeaderLength() const { return max_header_length; }
int GetMaxFileSize() const { return max_file_size; }
int GetStatInterval() const { return stat_interval; }
int GetExpiredTime() const { return expired_time; }

// --- new: setters ---
void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; }
void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; }

protected:
file_cache_ptr Get(const char* filepath);
};
Expand Down
Loading