Skip to content

Commit 92e532b

Browse files
committed
Improve asset fetching logic
/game_version: fix version field not matching binaries Fix sha256 checksum error blocking every release Added a retry policy
1 parent 5e496b0 commit 92e532b

4 files changed

Lines changed: 104 additions & 44 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ log = "0.4"
2121
octocrab = "0.43"
2222
rand_core = "0.6.4"
2323
reqwest = { version = "0.12", features = ["charset", "http2", "macos-system-configuration", "rustls-tls"], default-features = false }
24+
reqwest-middleware = "0.4"
25+
reqwest-retry = "0.7"
2426
secure-string = { version = "0.3", features = ["serde"] }
2527
semver = "1.0"
2628
serde = { version = "1.0", features = ["derive"] }

src/fetcher.rs

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
use std::time::Duration;
2+
13
use futures::future::join_all;
24
use octocrab::models::repos;
35
use octocrab::repos::RepoHandler;
46
use octocrab::{Octocrab, OctocrabBuilder};
7+
use reqwest::StatusCode;
58
use semver::Version;
69

710
use crate::config::ApiConfig;
811
use crate::errors::{InternalError, Result};
9-
use crate::game_data::{Asset, Assets, GameRelease, Repo};
12+
use crate::game_data::{Asset, AssetList, AssetPerPlatform, GameReleases, Repo};
1013

1114
pub struct Fetcher {
1215
octocrab: Octocrab,
@@ -16,7 +19,7 @@ pub struct Fetcher {
1619
checksum_fetcher: ChecksumFetcher,
1720
}
1821

19-
struct ChecksumFetcher(reqwest::Client);
22+
struct ChecksumFetcher(reqwest_middleware::ClientWithMiddleware);
2023

2124
impl Fetcher {
2225
pub fn from_config(config: &ApiConfig) -> Result<Self> {
@@ -38,11 +41,12 @@ impl Fetcher {
3841
self.octocrab.repos(repo.owner(), repo.repository())
3942
}
4043

41-
pub async fn get_latest_game_release(&self) -> Result<GameRelease> {
44+
pub async fn get_latest_game_releases(&self) -> Result<GameReleases> {
4245
let releases = self
4346
.on_repo(&self.game_repo)
4447
.releases()
4548
.list()
49+
.per_page(100)
4650
.send()
4751
.await?;
4852

@@ -58,36 +62,52 @@ impl Fetcher {
5862
let mut binaries = self
5963
.get_assets_and_checksums(&latest_release.assets, &latest_version, None)
6064
.await
61-
.map(|((platform, mut asset), sha256)| {
62-
asset.sha256 = sha256?;
63-
Ok((platform.to_string(), asset))
65+
.filter_map(|((platform, mut asset), sha256)| {
66+
match sha256 {
67+
Ok(checksum) => {
68+
asset.sha256 = checksum;
69+
Some(Ok((platform.to_string(), asset)))
70+
},
71+
Err(err) => {
72+
log::error!("ignoring asset {0} (version: {1}) because an error occurred for checksum: {2:?}", asset.name, asset.version, err);
73+
None
74+
}
75+
}
6476
})
65-
.collect::<Result<Assets>>()?;
77+
.collect::<Result<AssetPerPlatform>>()?;
78+
79+
let mut assets = AssetList::new();
6680

6781
for (version, release) in versions_released {
6882
for ((platform, mut asset), sha256) in self
6983
.get_assets_and_checksums(&release.assets, &version, Some(&binaries))
7084
.await
7185
{
72-
asset.sha256 = sha256?;
73-
binaries.insert(platform.to_string(), asset);
86+
match sha256 {
87+
Ok(checksum) => {
88+
asset.sha256 = checksum;
89+
90+
if platform == "assets" {
91+
assets.push(asset);
92+
} else {
93+
binaries.insert(platform.to_string(), asset);
94+
}
95+
},
96+
Err(err) => {
97+
log::error!("ignoring asset {0} (version: {1}) because an error occurred for checksum: {2:?}", asset.name, asset.version, err);
98+
}
99+
}
74100
}
75101
}
76102

77-
let latest_assets = binaries.remove("assets");
78-
79-
match latest_assets {
80-
Some(assets) => Ok(GameRelease {
81-
assets_version: assets.version.clone(),
82-
assets,
83-
binaries,
84-
version: latest_version,
85-
}),
86-
None => Err(InternalError::NoReleaseFound),
103+
if binaries.is_empty() {
104+
return Err(InternalError::NoReleaseFound);
87105
}
106+
107+
Ok(GameReleases { assets, binaries })
88108
}
89109

90-
pub async fn get_latest_updater_release(&self) -> Result<Assets> {
110+
pub async fn get_latest_updater_release(&self) -> Result<AssetPerPlatform> {
91111
let last_release = self
92112
.on_repo(&self.updater_repo)
93113
.releases()
@@ -98,18 +118,26 @@ impl Fetcher {
98118

99119
self.get_assets_and_checksums(&last_release.assets, &version, None)
100120
.await
101-
.map(|((platform, mut asset), sha256)| {
102-
asset.sha256 = sha256?;
103-
Ok((platform.to_string(), asset))
121+
.filter_map(|((platform, mut asset), sha256)| {
122+
match sha256 {
123+
Ok(checksum) => {
124+
asset.sha256 = checksum;
125+
Some(Ok((platform.to_string(), asset)))
126+
},
127+
Err(err) => {
128+
log::error!("ignoring updater {0} (version: {1}) because an error occurred for checksum: {2:?}", asset.name, asset.version, err);
129+
None
130+
}
131+
}
104132
})
105-
.collect::<Result<Assets>>()
133+
.collect::<Result<AssetPerPlatform>>()
106134
}
107135

108136
async fn get_assets_and_checksums<'a: 'b, 'b, A>(
109137
&self,
110138
assets: A,
111139
version: &Version,
112-
binaries: Option<&Assets>,
140+
binaries: Option<&AssetPerPlatform>,
113141
) -> impl Iterator<Item = ((&'b str, Asset), Result<Option<String>>)>
114142
where
115143
A: IntoIterator<Item = &'a repos::Asset>,
@@ -140,7 +168,16 @@ impl Fetcher {
140168

141169
impl ChecksumFetcher {
142170
fn new() -> Self {
143-
Self(reqwest::Client::new())
171+
let retry_policy = reqwest_retry::policies::ExponentialBackoff::builder()
172+
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(15));
173+
174+
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
175+
.with(reqwest_retry::RetryTransientMiddleware::new_with_policy(
176+
retry_policy,
177+
))
178+
.build();
179+
180+
Self(client)
144181
}
145182

146183
async fn resolve(&self, asset: &Asset) -> Result<Option<String>> {
@@ -151,10 +188,9 @@ impl ChecksumFetcher {
151188
.send()
152189
.await?;
153190

154-
match response.status().is_client_error() {
155-
// No SHA256 found
156-
true => Ok(None),
157-
false => {
191+
match response.status() {
192+
StatusCode::NOT_FOUND => Ok(None),
193+
_ => {
158194
let content = response.text().await?;
159195
self.parse_response(asset.name.as_str(), content.as_str())
160196
.map(Some)

src/game_data.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ pub struct Repo {
2121
repository: String,
2222
}
2323

24-
pub type Assets = HashMap<String, Asset>;
24+
pub type AssetList = Vec<Asset>;
25+
pub type AssetPerPlatform = HashMap<String /*platform*/, Asset>;
2526

2627
#[derive(Clone)]
27-
pub struct GameRelease {
28-
pub assets: Asset,
29-
pub assets_version: Version,
30-
pub binaries: Assets,
31-
pub version: Version,
28+
pub struct GameReleases {
29+
pub assets: AssetList,
30+
pub binaries: AssetPerPlatform,
3231
}
3332

3433
#[derive(Serialize)]

src/routes/version.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::app_data::AppData;
77
use crate::config::ApiConfig;
88
use crate::errors::api::{ErrorCause, RouteError};
99
use crate::errors::codes::ServerErrorCode;
10-
use crate::game_data::{Assets, GameRelease, GameVersion};
10+
use crate::game_data::{AssetPerPlatform, GameReleases, GameVersion};
1111

1212
#[derive(Deserialize)]
1313
struct VersionQuery {
@@ -16,8 +16,8 @@ struct VersionQuery {
1616

1717
#[derive(Clone)]
1818
pub(crate) enum CachedReleased {
19-
Updater(Assets),
20-
Game(GameRelease),
19+
Updater(AssetPerPlatform),
20+
Game(GameReleases),
2121
}
2222

2323
#[get("/game_version")]
@@ -40,6 +40,7 @@ async fn game_version(
4040
})
4141
.await
4242
.cloned();
43+
4344
let updater_releases = match results_updater_release {
4445
Ok(CachedReleased::Updater(updater_release)) => updater_release,
4546
Ok(CachedReleased::Game(_)) => unreachable!(),
@@ -48,9 +49,9 @@ async fn game_version(
4849

4950
// TODO: remove .cloned
5051
let results_game_release = cache
51-
.try_get_or_set_with("latest_game_release", || async {
52+
.try_get_or_set_with("latest_game_releases", || async {
5253
fetcher
53-
.get_latest_game_release()
54+
.get_latest_game_releases()
5455
.await
5556
.map(CachedReleased::Game)
5657
})
@@ -104,11 +105,33 @@ async fn game_version(
104105
}
105106
};
106107

108+
// Find first Asset matching game version
109+
let assets = match game_releases
110+
.assets
111+
.iter()
112+
.find(|assets| assets.version <= game_binary.version)
113+
{
114+
Some(assets) => assets,
115+
None => {
116+
let msg = format!(
117+
"No assets found matching version '{0}'",
118+
game_binary.version
119+
);
120+
log::error!("{msg}");
121+
return Err(RouteError::InvalidRequest(
122+
ServerErrorCode::NotFoundPlatform(platform),
123+
msg,
124+
));
125+
}
126+
};
127+
128+
let game_version = game_binary.version.clone();
129+
107130
Ok(HttpResponse::Ok().json(GameVersion {
108-
assets: game_releases.assets,
109-
assets_version: game_releases.assets_version.to_string(),
131+
assets: assets.clone(),
132+
assets_version: assets.version.to_string(),
110133
binaries: game_binary,
111134
updater: updater_binary,
112-
version: game_releases.version.to_string(),
135+
version: game_version.to_string(),
113136
}))
114137
}

0 commit comments

Comments
 (0)