singleton assets proxy

This commit is contained in:
nikita@galaiko.rocks 2023-07-17 15:26:49 +02:00 committed by Nikita Galaiko
parent 1300888d82
commit 922046d9c3
4 changed files with 89 additions and 50 deletions

1
src-tauri/Cargo.lock generated
View File

@ -1520,6 +1520,7 @@ dependencies = [
"tokio",
"tokio-tungstenite 0.18.0",
"tokio-util",
"url",
"urlencoding",
"uuid",
"walkdir",

View File

@ -54,6 +54,7 @@ refinery = { version = "0.8", features = [ "rusqlite" ] }
sha1 = "0.10.5"
tokio-util = "0.7.8"
diffy = "0.3.0"
url = "2.4.0"
[features]
# by default Tauri runs in production mode

76
src-tauri/src/assets.rs Normal file
View File

@ -0,0 +1,76 @@
use std::{collections::HashMap, path, sync};
use anyhow::Result;
use tokio::sync::Semaphore;
use url::Url;
pub struct Proxy {
cache_dir: path::PathBuf,
semaphores: sync::Arc<tokio::sync::Mutex<HashMap<url::Url, Semaphore>>>,
}
const ASSET_SCHEME: &str = "asset";
impl Proxy {
pub fn new<P: AsRef<path::Path>>(cache_dir: P) -> Self {
Self {
cache_dir: cache_dir.as_ref().to_path_buf(),
semaphores: sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())),
}
}
// takes a url of a remote assets, downloads it into cache and returns a url that points to the cached file
pub async fn proxy(&self, src: &Url) -> Result<Url> {
if src.scheme() == ASSET_SCHEME {
return Ok(src.clone());
}
let hash = md5::compute(src.to_string());
let path = path::Path::new(src.path());
let ext = path.extension().unwrap().to_str().unwrap();
let save_to = self.cache_dir.join(format!("{:X}.{}", hash, ext));
if save_to.exists() {
return Ok(build_asset_url(&save_to.display().to_string()));
}
// only one download per url at a time
let mut semaphores = self.semaphores.lock().await;
let r = semaphores
.entry(src.clone())
.or_insert_with(|| Semaphore::new(1));
let _permit = r.acquire().await?;
if save_to.exists() {
// check again, maybe url was downloaded
return Ok(build_asset_url(&save_to.display().to_string()));
}
log::info!("Downloading image {}", src);
let resp = reqwest::get(src.clone()).await?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to download image {}: {}",
src,
resp.status()
));
}
let bytes = resp.bytes().await?;
std::fs::create_dir_all(&self.cache_dir)?;
std::fs::write(&save_to, bytes)?;
Ok(build_asset_url(&save_to.display().to_string()))
}
}
fn build_asset_url(path: &str) -> Url {
Url::parse(&format!(
"{}://localhost/{}",
ASSET_SCHEME,
urlencoding::encode(path)
))
.unwrap()
}

View File

@ -2,6 +2,7 @@
extern crate scopeguard;
mod app;
mod assets;
mod bookmarks;
mod database;
mod deltas;
@ -77,46 +78,6 @@ impl From<anyhow::Error> for Error {
const IS_DEV: bool = cfg!(debug_assertions);
fn build_asset_url(path: &str) -> String {
format!("asset://localhost/{}", urlencoding::encode(path))
}
#[timed(duration(printer = "debug!"))]
async fn proxy_image(handle: tauri::AppHandle, src: &str) -> Result<String> {
if src.starts_with("asset://") {
return Ok(src.to_string());
}
let images_dir = handle
.path_resolver()
.app_cache_dir()
.unwrap()
.join("images");
let hash = md5::compute(src);
let ext = src.split('.').last().unwrap_or("jpg");
let save_to = images_dir.join(format!("{:X}.{}", hash, ext));
if save_to.exists() {
return Ok(build_asset_url(&save_to.display().to_string()));
}
let resp = reqwest::get(src).await?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to download image {}: {}",
src,
resp.status()
));
}
let bytes = resp.bytes().await?;
std::fs::create_dir_all(&images_dir)?;
std::fs::write(&save_to, bytes)?;
Ok(build_asset_url(&save_to.display().to_string()))
}
#[timed(duration(printer = "debug!"))]
#[tauri::command(async)]
async fn get_project_archive_path(
@ -217,19 +178,21 @@ async fn list_sessions(
#[tauri::command(async)]
async fn get_user(handle: tauri::AppHandle) -> Result<Option<users::User>, Error> {
let app = handle.state::<app::App>();
let proxy = handle.state::<assets::Proxy>();
match app.get_user().context("failed to get user")? {
Some(user) => {
let local_picture = match proxy_image(handle, &user.picture).await {
let remote_picture = url::Url::parse(&user.picture).context("invalid picture url")?;
let local_picture = match proxy.proxy(&remote_picture).await {
Ok(picture) => picture,
Err(e) => {
log::error!("{:#}", e);
user.picture
remote_picture
}
};
let user = users::User {
picture: local_picture,
picture: local_picture.to_string(),
..user
};
@ -814,14 +777,12 @@ fn main() {
// debug_test_consistency(&app_state, "fec3d50c-503f-4021-89fb-e7ec2433ceae")
// .expect("FAIL");
let zipper = zip::Zipper::new(
tauri_app
.path_resolver()
.app_cache_dir()
.unwrap()
.join("archives"),
);
let cache_dir = tauri_app.path_resolver().app_cache_dir().unwrap();
let zipper = zip::Zipper::new(cache_dir.join("archives"));
let proxy = assets::Proxy::new(cache_dir.join("images"));
tauri_app.manage(zipper);
tauri_app.manage(proxy);
tauri_app.manage(app);
let app_handle = tauri_app.handle();