178 lines
5.0 KiB
Rust
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(),
|
|
))
|
|
}
|
|
}
|