Files
drop/desktop/src-tauri/download_manager/src/depot_manager.rs
T
2026-04-03 01:25:10 +00:00

178 lines
5.0 KiB
Rust

use std::{
collections::HashMap,
env,
sync::RwLock,
time::{Duration, Instant},
usize,
};
use futures_util::StreamExt;
use log::{info, warn};
use remote::{
error::RemoteAccessError,
requests::{generate_url, make_authenticated_get},
utils::DROP_CLIENT_ASYNC,
};
use serde::Deserialize;
use tauri::Url;
use crate::util::semaphore::{SyncSemaphore, SyncSemaphorePermit};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DepotManifestContent {
version_id: String,
//compression: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DepotManifest {
content: HashMap<String, Vec<DepotManifestContent>>,
}
struct Depot {
endpoint: String,
manifest: Option<DepotManifest>,
latest_speed: Option<usize>, // bytes per second
current_downloads: SyncSemaphore,
enabled: bool,
}
pub struct DepotManager {
depots: RwLock<Vec<Depot>>,
}
#[derive(Deserialize)]
struct ServersideDepot {
endpoint: String,
}
const SPEEDTEST_TIMEOUT: Duration = Duration::from_secs(4);
impl Default for DepotManager {
fn default() -> Self {
Self::new()
}
}
impl DepotManager {
pub fn new() -> Self {
Self {
depots: RwLock::new(Vec::new()),
}
}
async fn sync_depot(&self, depot: &mut Depot) -> Result<(), RemoteAccessError> {
let manifest_url = Url::parse(&depot.endpoint)?.join("manifest.json")?;
let manifest = DROP_CLIENT_ASYNC.get(manifest_url).send().await?;
let manifest: DepotManifest = manifest.json().await?;
depot.manifest.replace(manifest);
let speedtest_url = Url::parse(&depot.endpoint)?.join("speedtest")?;
let speedtest = DROP_CLIENT_ASYNC.get(speedtest_url).send().await?;
let mut stream = speedtest.bytes_stream();
let start = Instant::now();
let mut total_length = 0;
while let Some(chunk) = stream.next().await {
let length = chunk?.len();
total_length += length;
if SPEEDTEST_TIMEOUT <= start.elapsed() {
break;
}
}
let elapsed = start.elapsed().as_millis() as usize;
let speed = total_length.checked_div(elapsed).unwrap_or(usize::MAX);
depot.latest_speed.replace(speed);
Ok(())
}
pub async fn sync_depots(&self) -> Result<(), RemoteAccessError> {
let depots = make_authenticated_get(generate_url(&["/api/v1/client/depots"], &[])?).await?;
let depots: Vec<ServersideDepot> = depots.json().await?;
let mut new_depots = depots
.into_iter()
.map(|depot| Depot {
endpoint: if depot.endpoint.ends_with("/") {
depot.endpoint
} else {
format!("{}/", depot.endpoint)
},
manifest: None,
latest_speed: None,
current_downloads: SyncSemaphore::new(),
enabled: true,
})
.collect::<Vec<Depot>>();
info!("syncing {} depots...", new_depots.len());
for depot in &mut new_depots {
if let Err(sync_error) = self.sync_depot(depot).await {
warn!("failed to sync depot {}: {:?}", depot.endpoint, sync_error);
if env::var("FORCE_ENABLE_DEPOTS")
.map(|v| !v.is_empty())
.unwrap_or(false)
{
} else {
depot.enabled = false;
}
}
}
let enabled = new_depots.iter().filter(|v| v.enabled).count();
if enabled == 0 {
return Err(RemoteAccessError::NoDepots);
}
let mut depot_lock = self.depots.write().unwrap();
*depot_lock = new_depots;
Ok(())
}
pub fn next_depot(
&self,
game_id: &str,
version_id: &str,
) -> Result<(String, SyncSemaphorePermit), RemoteAccessError> {
let lock = self.depots.read().unwrap();
let best_depot = lock
.iter()
.filter(|v| {
let manifest = match &v.manifest {
Some(v) => v,
None => return false,
};
let versions = match manifest.content.get(game_id) {
Some(v) => v,
None => return false,
};
let _version = match versions.iter().find(|v| v.version_id == version_id) {
Some(v) => v,
None => return false,
};
true
})
.max_by(|x, y| {
let x_speed = x.latest_speed.unwrap_or(0) / x.current_downloads.permits();
let y_speed = y.latest_speed.unwrap_or(0) / y.current_downloads.permits();
x_speed.cmp(&y_speed)
})
.ok_or(RemoteAccessError::NoDepots)?;
Ok((
best_depot.endpoint.clone(),
best_depot.current_downloads.acquire(),
))
}
}