#include "CacheManager.hpp" #include #include #include #include #include #include #include #include #include #include #include static const char* TAG = "CacheManager"; struct DownloadArgs { std::string url; std::function onComplete; }; CacheManager::CacheManager() { mkdir(CACHE_DIR, 0777); } /* djb2 hash of the URL, returned as 8-char hex string */ std::string CacheManager::urlToKey(const std::string& url) const { uint32_t hash = 5381; for(char c : url) { hash = ((hash << 5) + hash) ^ (uint8_t)c; } char buf[9]; snprintf(buf, sizeof(buf), "%08lx", (unsigned long)hash); return std::string(buf); } std::string CacheManager::urlExtension(const std::string& url) const { /* Strip query/fragment before looking for extension */ size_t qs = url.find('?'); std::string path = (qs != std::string::npos) ? url.substr(0, qs) : url; size_t dot = path.rfind('.'); size_t slash = path.rfind('/'); if(dot != std::string::npos && (slash == std::string::npos || dot > slash)) { std::string ext = path.substr(dot); /* includes the dot */ if(ext.size() <= 5) return ext; /* ignore suspiciously long "extensions" */ } return ".mp3"; /* sensible default */ } std::string CacheManager::keyToPath(const std::string& key, const std::string& ext) const { return std::string(CACHE_DIR) + "/" + key + ext; } /* Find any file in CACHE_DIR whose name starts with key */ std::string CacheManager::findFileByKey(const std::string& key) const { DIR* dir = opendir(CACHE_DIR); if(!dir) return ""; struct dirent* entry; while((entry = readdir(dir)) != NULL) { std::string name(entry->d_name); if(name.rfind(key, 0) == 0) { closedir(dir); return std::string(CACHE_DIR) + "/" + name; } } closedir(dir); return ""; } std::string CacheManager::getCachedPath(const std::string& url) const { if(url.empty()) return ""; std::string key = urlToKey(url); std::string path = findFileByKey(key); if(!path.empty()) { ESP_LOGI(TAG, "Cache hit: %s -> %s", url.c_str(), path.c_str()); } return path; } void CacheManager::downloadTask(void* arg) { auto* dl = static_cast(arg); ESP_LOGI(TAG, "Downloading: %s", dl->url.c_str()); CacheManager cm; std::string key = cm.urlToKey(dl->url); std::string ext = cm.urlExtension(dl->url); std::string path = cm.keyToPath(key, ext); esp_http_client_config_t cfg = {}; cfg.url = dl->url.c_str(); cfg.timeout_ms = 30000; cfg.buffer_size = 4096; esp_http_client_handle_t client = esp_http_client_init(&cfg); esp_err_t err = esp_http_client_open(client, 0); bool success = false; if(err == ESP_OK) { int content_len = esp_http_client_fetch_headers(client); (void)content_len; FILE* f = fopen(path.c_str(), "wb"); if(f) { char buf[4096]; int read_len; while((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) { fwrite(buf, 1, read_len, f); } fclose(f); success = (read_len == 0); /* 0 = clean EOF */ if(!success) { remove(path.c_str()); ESP_LOGE(TAG, "Download error, partial file deleted"); } else { ESP_LOGI(TAG, "Cached to %s", path.c_str()); } } else { ESP_LOGE(TAG, "Cannot open cache file: %s (errno %d)", path.c_str(), errno); } } else { ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err)); } esp_http_client_cleanup(client); if(dl->onComplete) dl->onComplete(success); delete dl; vTaskDelete(NULL); } void CacheManager::downloadAsync(const std::string& url, std::function onComplete) { if(url.empty()) return; auto* args = new DownloadArgs{ url, onComplete }; xTaskCreate(downloadTask, "cache_dl", 8192, args, 3, NULL); } std::vector CacheManager::listEntries() const { std::vector entries; DIR* dir = opendir(CACHE_DIR); if(!dir) return entries; struct dirent* entry; while((entry = readdir(dir)) != NULL) { if(entry->d_name[0] == '.') continue; std::string name(entry->d_name); std::string full = std::string(CACHE_DIR) + "/" + name; struct stat st; size_t size = 0; if(stat(full.c_str(), &st) == 0) size = st.st_size; /* Key is filename without extension */ size_t dot = name.rfind('.'); std::string key = (dot != std::string::npos) ? name.substr(0, dot) : name; entries.push_back({ key, /*url unknown*/"", full, size }); } closedir(dir); return entries; } bool CacheManager::deleteEntry(const std::string& key) { std::string path = findFileByKey(key); if(path.empty()) return false; return remove(path.c_str()) == 0; } void CacheManager::deleteAll() { DIR* dir = opendir(CACHE_DIR); if(!dir) return; struct dirent* entry; while((entry = readdir(dir)) != NULL) { if(entry->d_name[0] == '.') continue; std::string full = std::string(CACHE_DIR) + "/" + entry->d_name; remove(full.c_str()); } closedir(dir); ESP_LOGI(TAG, "Cache cleared"); } size_t CacheManager::totalBytes() const { size_t total = 0; DIR* dir = opendir(CACHE_DIR); if(!dir) return 0; struct dirent* entry; while((entry = readdir(dir)) != NULL) { if(entry->d_name[0] == '.') continue; std::string full = std::string(CACHE_DIR) + "/" + entry->d_name; struct stat st; if(stat(full.c_str(), &st) == 0) total += st.st_size; } closedir(dir); return total; }