1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2738,6 +2738,7 @@ dependencies = [
|
||||
"dirs",
|
||||
"eframe",
|
||||
"egui",
|
||||
"ehttp",
|
||||
"enostr",
|
||||
"hex",
|
||||
"image",
|
||||
|
||||
@@ -26,6 +26,7 @@ puffin = { workspace = true, optional = true }
|
||||
puffin_egui = { workspace = true, optional = true }
|
||||
sha2 = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
ehttp = {workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -21,6 +21,7 @@ mod timecache;
|
||||
mod timed_serializer;
|
||||
pub mod ui;
|
||||
mod unknowns;
|
||||
mod urls;
|
||||
mod user_account;
|
||||
|
||||
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
|
||||
|
||||
214
crates/notedeck/src/urls.rs
Normal file
214
crates/notedeck/src/urls.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use egui::TextBuffer;
|
||||
use poll_promise::Promise;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
const FILE_NAME: &str = "urls.bin";
|
||||
const SAVE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
type UrlsToMime = HashMap<String, String>;
|
||||
|
||||
/// caches mime type for a URL. saves to disk on interval [`SAVE_INTERVAL`]
|
||||
pub struct UrlCache {
|
||||
last_saved: SystemTime,
|
||||
path: PathBuf,
|
||||
cache: Arc<RwLock<UrlsToMime>>,
|
||||
from_disk_promise: Option<Promise<Option<UrlsToMime>>>,
|
||||
}
|
||||
|
||||
impl UrlCache {
|
||||
pub fn rel_dir() -> &'static str {
|
||||
FILE_NAME
|
||||
}
|
||||
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
last_saved: SystemTime::now(),
|
||||
path: path.clone(),
|
||||
cache: Default::default(),
|
||||
from_disk_promise: Some(read_from_disk(path)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(&self, url: &str) -> Option<String> {
|
||||
self.cache.read().ok()?.get(url).cloned()
|
||||
}
|
||||
|
||||
pub fn set_type(&mut self, url: String, mime_type: String) {
|
||||
if let Ok(mut locked_cache) = self.cache.write() {
|
||||
locked_cache.insert(url, mime_type);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_io(&mut self) {
|
||||
if let Some(promise) = &mut self.from_disk_promise {
|
||||
if let Some(maybe_cache) = promise.ready_mut() {
|
||||
if let Some(cache) = maybe_cache.take() {
|
||||
merge_cache(self.cache.clone(), cache)
|
||||
}
|
||||
|
||||
self.from_disk_promise = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(cur_duration) = SystemTime::now().duration_since(self.last_saved) {
|
||||
if cur_duration >= SAVE_INTERVAL {
|
||||
save_to_disk(self.path.clone(), self.cache.clone());
|
||||
self.last_saved = SystemTime::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_cache(cur_cache: Arc<RwLock<UrlsToMime>>, from_disk: UrlsToMime) {
|
||||
std::thread::spawn(move || {
|
||||
if let Ok(mut locked_cache) = cur_cache.write() {
|
||||
locked_cache.extend(from_disk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn read_from_disk(path: PathBuf) -> Promise<Option<UrlsToMime>> {
|
||||
let (sender, promise) = Promise::new();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let result: Result<UrlsToMime, Error> = (|| {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
let data: UrlsToMime =
|
||||
bincode::deserialize(&buffer).map_err(|e| Error::Generic(e.to_string()))?;
|
||||
Ok(data)
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(data) => sender.send(Some(data)),
|
||||
Err(e) => {
|
||||
tracing::error!("problem deserializing UrlCache: {e}");
|
||||
sender.send(None)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
promise
|
||||
}
|
||||
|
||||
fn save_to_disk(path: PathBuf, cache: Arc<RwLock<UrlsToMime>>) {
|
||||
std::thread::spawn(move || {
|
||||
let result: Result<(), Error> = (|| {
|
||||
if let Ok(cache) = cache.read() {
|
||||
let cache = &*cache;
|
||||
let encoded =
|
||||
bincode::serialize(cache).map_err(|e| Error::Generic(e.to_string()))?;
|
||||
let mut file = File::create(&path)?;
|
||||
file.write_all(&encoded)?;
|
||||
file.sync_all()?;
|
||||
tracing::info!("Saved UrlCache to disk.");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Generic(
|
||||
"Could not read UrlCache behind RwLock".to_owned(),
|
||||
))
|
||||
}
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Failed to save UrlCache: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn ehttp_get_mime_type(url: &str, sender: poll_promise::Sender<MimeResult>) {
|
||||
let request = ehttp::Request::head(url);
|
||||
|
||||
let url = url.to_owned();
|
||||
ehttp::fetch(
|
||||
request,
|
||||
move |response: Result<ehttp::Response, String>| match response {
|
||||
Ok(resp) => {
|
||||
if let Some(content_type) = resp.headers.get("content-type") {
|
||||
sender.send(MimeResult::Ok(extract_mime_type(content_type).to_owned()));
|
||||
} else {
|
||||
sender.send(MimeResult::Err(HttpError::MissingHeader));
|
||||
tracing::error!("Content-Type header not found for {url}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
sender.send(MimeResult::Err(HttpError::HttpFailure));
|
||||
tracing::error!("failed ehttp for UrlCache: {err}");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum HttpError {
|
||||
HttpFailure,
|
||||
MissingHeader,
|
||||
}
|
||||
|
||||
type MimeResult = Result<String, HttpError>;
|
||||
|
||||
fn extract_mime_type(content_type: &str) -> &str {
|
||||
content_type
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap_or(content_type)
|
||||
.trim()
|
||||
}
|
||||
|
||||
pub struct UrlMimes {
|
||||
pub cache: UrlCache,
|
||||
in_flight: HashMap<String, Promise<MimeResult>>,
|
||||
}
|
||||
|
||||
impl UrlMimes {
|
||||
pub fn new(url_cache: UrlCache) -> Self {
|
||||
Self {
|
||||
cache: url_cache,
|
||||
in_flight: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, url: &str) -> Option<String> {
|
||||
if let Some(mime_type) = self.cache.get_type(url) {
|
||||
Some(mime_type)
|
||||
} else if let Some(promise) = self.in_flight.get_mut(url) {
|
||||
if let Some(mime_result) = promise.ready_mut() {
|
||||
match mime_result {
|
||||
Ok(mime_type) => {
|
||||
let mime_type = mime_type.take();
|
||||
self.cache.set_type(url.to_owned(), mime_type.clone());
|
||||
self.in_flight.remove(url);
|
||||
Some(mime_type)
|
||||
}
|
||||
Err(HttpError::HttpFailure) => {
|
||||
// allow retrying
|
||||
self.in_flight.remove(url);
|
||||
None
|
||||
}
|
||||
Err(HttpError::MissingHeader) => {
|
||||
// response was malformed, don't retry
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let (sender, promise) = Promise::new();
|
||||
ehttp_get_mime_type(url, sender);
|
||||
self.in_flight.insert(url.to_owned(), promise);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user