Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src-tauri/src/buzzheavier.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::download_resolver::{
Provider, ResolveError, ResolvedDownload, ResolvedFile, network_error,
Provider, ResolveError, ResolvedDownload, ResolvedFile, network_error, retry_after,
};
use regex::Regex;
use reqwest::{
Expand Down Expand Up @@ -82,9 +82,10 @@ async fn fetch(client: &Client, url: &str, headers: HeaderMap) -> Result<Respons
if !is_challenge(response.headers()) {
return match response.status() {
StatusCode::NOT_FOUND => Err("The Buzzheavier file was not found".into()),
StatusCode::TOO_MANY_REQUESTS => {
Err("Buzzheavier rate limit reached, try again later".into())
}
StatusCode::TOO_MANY_REQUESTS => Err(ResolveError::RateLimited {
message: "Buzzheavier rate limit reached, try again later".into(),
retry_after: retry_after(response.headers()),
}),
status if status.is_success() => Ok(response),
status => Err(ResolveError::Http(status)),
};
Expand All @@ -93,7 +94,10 @@ async fn fetch(client: &Client, url: &str, headers: HeaderMap) -> Result<Respons
tokio::time::sleep(RETRY_DELAY).await;
}
}
Err("Buzzheavier is asking for a browser check, try again later".into())
Err(ResolveError::RateLimited {
message: "Buzzheavier is asking for a browser check, try again later".into(),
retry_after: None,
})
}

pub(crate) async fn resolve(client: &Client, uri: &str) -> Result<ResolvedDownload, ResolveError> {
Expand Down
22 changes: 19 additions & 3 deletions src-tauri/src/download_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,11 @@ enum TransferError {
Network(String),
Server(String),
Link(String),
/// The host asked to wait before the next attempt on this link.
RateLimited {
message: String,
delay: Duration,
},
}

impl std::fmt::Display for TransferError {
Expand All @@ -1266,7 +1271,8 @@ impl std::fmt::Display for TransferError {
Self::Local(message)
| Self::Network(message)
| Self::Server(message)
| Self::Link(message) => formatter.write_str(message),
| Self::Link(message)
| Self::RateLimited { message, .. } => formatter.write_str(message),
}
}
}
Expand All @@ -1278,6 +1284,10 @@ impl From<ResolveError> for TransferError {
ResolveError::Network(_) => Self::Network(message),
ResolveError::Http(status) if status.is_server_error() => Self::Server(message),
ResolveError::Http(_) | ResolveError::Link(_) => Self::Link(message),
ResolveError::RateLimited { retry_after, .. } => Self::RateLimited {
message,
delay: crate::download_selection::rate_limit_delay(retry_after),
},
}
}
}
Expand Down Expand Up @@ -1349,6 +1359,7 @@ async fn transfer<F: std::future::Future<Output = bool>>(
let mut current = job.clone();
let mut failure = "No supported download links are available".to_owned();
while let Some(uri) = plan.current().map(str::to_owned) {
let mut retry_delay = Duration::from_millis(250);
if *stopped.borrow() {
return Err(TransferError::Stopped);
}
Expand Down Expand Up @@ -1395,15 +1406,20 @@ async fn transfer<F: std::future::Future<Output = bool>>(
Err(error @ TransferError::Network(_)) if !online().await => {
return Err(error);
}
Err(error) => failure = error.to_string(),
Err(error) => {
if let TransferError::RateLimited { delay, .. } = &error {
retry_delay = *delay;
}
failure = error.to_string();
}
}
plan.fail();
save_mirror_state(database.clone(), &current, &plan, false).await?;
if plan.current().is_some() {
tokio::select! {
biased;
_ = stopped.changed() => return Err(TransferError::Stopped),
() = tokio::time::sleep(Duration::from_millis(250)) => {},
() = tokio::time::sleep(retry_delay) => {},
}
}
}
Expand Down
42 changes: 38 additions & 4 deletions src-tauri/src/download_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,32 @@ pub(crate) enum ResolveError {
Network(String),
Http(StatusCode),
Link(String),
/// The host asks to try again later; `retry_after` is its stated wait, if any.
RateLimited {
message: String,
retry_after: Option<Duration>,
},
}

impl ResolveError {
/// Failures that say nothing lasting about the link, so retrying later can succeed.
pub(crate) fn is_transient(&self) -> bool {
match self {
Self::Network(_) | Self::RateLimited { .. } => true,
Self::Http(status) => {
status.is_server_error() || matches!(status.as_u16(), 408 | 425 | 429)
}
Self::Link(_) => false,
}
}
}

impl std::fmt::Display for ResolveError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Network(message) => write!(formatter, "Network: {message}"),
Self::Http(status) => write!(formatter, "HTTP {}", status.as_u16()),
Self::Link(message) => formatter.write_str(message),
Self::Link(message) | Self::RateLimited { message, .. } => formatter.write_str(message),
}
}
}
Expand All @@ -74,6 +92,18 @@ impl From<ResolveError> for String {
}
}

/// Seconds form of `Retry-After`; the date form is ignored.
pub(crate) fn retry_after(headers: &HeaderMap) -> Option<Duration> {
headers
.get(reqwest::header::RETRY_AFTER)?
.to_str()
.ok()?
.trim()
.parse()
.ok()
.map(Duration::from_secs)
}

pub(crate) fn network_error(error: reqwest::Error) -> ResolveError {
ResolveError::Network(error.without_url().to_string())
}
Expand Down Expand Up @@ -179,6 +209,12 @@ async fn follow_redirects(
url = next;
continue;
}
if response.status() == StatusCode::TOO_MANY_REQUESTS {
return Err(ResolveError::RateLimited {
message: "The download host is limiting requests, try again later".into(),
retry_after: retry_after(response.headers()),
});
}
if !response.status().is_success() {
return Err(ResolveError::Http(response.status()));
}
Expand Down Expand Up @@ -410,9 +446,7 @@ async fn resolve_gofile(client: &Client, uri: &str) -> Result<ResolvedDownload,
.query_pairs()
.find(|(key, _)| key == "password")
.map(|(_, value)| value.into_owned());
crate::gofile::resolve(client, id, password.as_deref())
.await
.map_err(ResolveError::Link)
crate::gofile::resolve(client, id, password.as_deref()).await
}

#[cfg(test)]
Expand Down
27 changes: 26 additions & 1 deletion src-tauri/src/download_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ impl MirrorPlan {
}
}

/// Pause before retrying a link whose host asked to wait. Without a stated wait
/// the host gets a short default, and long waits are cut so the job stays responsive.
pub(crate) fn rate_limit_delay(retry_after: Option<Duration>) -> Duration {
retry_after
.unwrap_or(Duration::from_secs(30))
.clamp(Duration::from_secs(1), Duration::from_secs(300))
}

pub(crate) fn native_cdn(uri: &str) -> bool {
reqwest::Url::parse(uri)
.is_ok_and(|url| url.scheme() == "https" && url.host_str() == Some("cdn.taxphobia.top"))
Expand All @@ -63,7 +71,7 @@ pub(crate) async fn inspect(client: &reqwest::Client, uri: &str) -> Result<u64,
};
tokio::time::timeout(timeout, inspect_file(client, uri))
.await
.map_err(|_| ResolveError::Link("Download host lookup timed out".to_owned()))?
.map_err(|_| ResolveError::Network("Download host lookup timed out".to_owned()))?
}

async fn inspect_file(client: &reqwest::Client, uri: &str) -> Result<u64, ResolveError> {
Expand Down Expand Up @@ -140,6 +148,23 @@ async fn inspect_file(client: &reqwest::Client, uri: &str) -> Result<u64, Resolv
mod tests {
use super::*;

#[test]
fn rate_limit_delay_respects_the_host_within_bounds() {
assert_eq!(rate_limit_delay(None), Duration::from_secs(30));
assert_eq!(
rate_limit_delay(Some(Duration::from_secs(90))),
Duration::from_secs(90)
);
assert_eq!(
rate_limit_delay(Some(Duration::ZERO)),
Duration::from_secs(1)
);
assert_eq!(
rate_limit_delay(Some(Duration::from_secs(3600))),
Duration::from_secs(300)
);
}

#[test]
fn source_size_allows_exactly_three_percent_without_overflow() {
assert!(size_matches(Some(1000), 970));
Expand Down
Loading
Loading