#include "services/textureCache.hpp" #include #include #include #include #include #include #include #include #include #include "giomm/file.h" #include "glibmm/bytes.h" namespace { #define CACHE_SIZE 128 #define CACHE_AGE 168 constexpr std::uint64_t kFnvOffsetBasis = 14695981039346656037ull; constexpr std::uint64_t kFnvPrime = 1099511628211ull; std::string to_hex(std::uint64_t value) { std::ostringstream stream; stream << std::hex << std::setw(16) << std::setfill('0') << value; return stream.str(); } std::string hash_url(const std::string &url) { std::uint64_t hash = kFnvOffsetBasis; for (unsigned char c : url) { hash ^= c; hash *= kFnvPrime; } return to_hex(hash); } std::filesystem::path get_cache_dir() { const char *xdg_cache = std::getenv("XDG_CACHE_HOME"); if (xdg_cache && *xdg_cache) { return std::filesystem::path(xdg_cache) / "bar"; } const char *home = std::getenv("HOME"); if (home && *home) { return std::filesystem::path(home) / ".cache" / "bar"; } return std::filesystem::temp_directory_path() / "bar"; } std::filesystem::path get_cache_path_for_url(const std::string &url) { auto filename = hash_url(url); auto last_slash = url.find_last_of('/'); auto last_dot = url.find_last_of('.'); if (last_dot != std::string::npos && (last_slash == std::string::npos || last_dot > last_slash)) { auto ext = url.substr(last_dot); if (ext.size() <= 10) { filename += ext; } } return get_cache_dir() / filename; } std::chrono::system_clock::time_point to_system_clock(std::filesystem::file_time_type time) { return std::chrono::time_point_cast( time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now() ); } size_t write_to_buffer(void *contents, size_t size, size_t nmemb, void *userp) { auto *buffer = static_cast *>(userp); auto total = size * nmemb; auto *bytes = static_cast(contents); buffer->insert(buffer->end(), bytes, bytes + total); return total; } Glib::RefPtr load_texture_from_file(const std::filesystem::path &path) { if (!std::filesystem::exists(path)) { return {}; } auto file = Gio::File::create_for_path(path.string()); try { return Gdk::Texture::create_from_file(file); } catch (...) { return {}; } } Glib::RefPtr download_texture_from_url(const std::string &url, const std::filesystem::path &path) { if (url.empty()) { return {}; } CURL *curl = curl_easy_init(); if (!curl) { return {}; } std::vector buffer; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_buffer); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, "bar/1.0"); auto res = curl_easy_perform(curl); curl_easy_cleanup(curl); if (res != CURLE_OK || buffer.empty()) { return {}; } std::error_code error; std::filesystem::create_directories(path.parent_path(), error); std::ofstream output(path, std::ios::binary | std::ios::trunc); if (output) { output.write(reinterpret_cast(buffer.data()), static_cast(buffer.size())); } auto bytes = Glib::Bytes::create(buffer.data(), buffer.size()); return Gdk::Texture::create_from_bytes(bytes); } } // namespace TextureCacheService *TextureCacheService::getInstance() { static TextureCacheService instance; return &instance; } Glib::RefPtr TextureCacheService::getTexture(const std::string &url) { if (url.empty()) { return {}; } auto it = cache.find(url); if (it != cache.end()) { return it->second; } auto cache_path = get_cache_path_for_url(url); auto texture = load_texture_from_file(cache_path); if (!texture) { texture = download_texture_from_url(url, cache_path); } if (texture) { cache.emplace(url, texture); std::error_code error; std::filesystem::last_write_time(cache_path, std::filesystem::file_time_type::clock::now(), error); } return texture; } void TextureCacheService::pruneCache() { std::error_code error; auto cache_dir = get_cache_dir(); std::filesystem::create_directories(cache_dir, error); std::vector> files; std::uintmax_t total_size = 0; auto now = std::chrono::system_clock::now(); auto max_age = std::chrono::hours(CACHE_AGE); for (const auto &entry : std::filesystem::directory_iterator(cache_dir, error)) { if (error || !entry.is_regular_file()) { continue; } auto path = entry.path(); auto last_write = entry.last_write_time(error); if (!error) { auto age = now - to_system_clock(last_write); if (age > max_age) { std::filesystem::remove(path, error); continue; } } auto size = entry.file_size(error); if (!error) { total_size += size; files.emplace_back(path, size); } } auto max_size = static_cast(CACHE_SIZE) * 1024ull * 1024ull; if (total_size <= max_size) { return; } std::sort(files.begin(), files.end(), [&](const auto &left, const auto &right) { std::error_code left_error; std::error_code right_error; auto left_time = std::filesystem::last_write_time(left.first, left_error); auto right_time = std::filesystem::last_write_time(right.first, right_error); if (left_error || right_error) { return left.first.string() < right.first.string(); } return left_time < right_time; }); for (const auto &item : files) { if (total_size <= max_size) { break; } std::filesystem::remove(item.first, error); if (!error) { if (total_size >= item.second) { total_size -= item.second; } else { total_size = 0; } } } }