224 lines
6.4 KiB
C++
224 lines
6.4 KiB
C++
#include "services/textureCache.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <curl/curl.h>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#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<std::chrono::system_clock::duration>(
|
|
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<std::vector<unsigned char> *>(userp);
|
|
auto total = size * nmemb;
|
|
auto *bytes = static_cast<unsigned char *>(contents);
|
|
buffer->insert(buffer->end(), bytes, bytes + total);
|
|
return total;
|
|
}
|
|
|
|
Glib::RefPtr<Gdk::Texture> 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<Gdk::Texture> 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<unsigned char> 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<const char *>(buffer.data()), static_cast<std::streamsize>(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<Gdk::Texture> 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<std::pair<std::filesystem::path, std::uintmax_t>> 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<std::uintmax_t>(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;
|
|
}
|
|
}
|
|
}
|
|
}
|