You've already forked AstralRinth
forked from didirus/AstralRinth
Merge tag 'v0.10.27' into beta
This commit is contained in:
12
packages/app-lib/.sqlx/query-175067f04e775f5469146f3cb77c422c3ab7203409083fd3c9c968b00b46918f.json
generated
Normal file
12
packages/app-lib/.sqlx/query-175067f04e775f5469146f3cb77c422c3ab7203409083fd3c9c968b00b46918f.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n locale = $4,\n default_page = $5,\n collapsed_navigation = $6,\n advanced_rendering = $7,\n native_decorations = $8,\n\n discord_rpc = $9,\n developer_mode = $10,\n telemetry = $11,\n personalized_ads = $12,\n\n onboarded = $13,\n\n extra_launch_args = jsonb($14),\n custom_env_vars = jsonb($15),\n mc_memory_max = $16,\n mc_force_fullscreen = $17,\n mc_game_resolution_x = $18,\n mc_game_resolution_y = $19,\n hide_on_process_start = $20,\n\n hook_pre_launch = $21,\n hook_wrapper = $22,\n hook_post_exit = $23,\n\n custom_dir = $24,\n prev_custom_dir = $25,\n migrated = $26,\n\n toggle_sidebar = $27,\n feature_flags = $28,\n hide_nametag_skins_page = $29,\n\n skipped_update = $30,\n pending_update_toast_for_version = $31,\n auto_download_updates = $32,\n\n version = $33\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 33
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "175067f04e775f5469146f3cb77c422c3ab7203409083fd3c9c968b00b46918f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates,\n version\n FROM settings\n ",
|
||||
"query": "\n SELECT\n max_concurrent_writes, max_concurrent_downloads,\n theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,\n discord_rpc, developer_mode, telemetry, personalized_ads,\n onboarded,\n json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,\n mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,\n hook_pre_launch, hook_wrapper, hook_post_exit,\n custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,\n skipped_update, pending_update_toast_for_version, auto_download_updates,\n version\n FROM settings\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,148 +19,153 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "default_page",
|
||||
"name": "locale",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "collapsed_navigation",
|
||||
"name": "default_page",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "hide_nametag_skins_page",
|
||||
"name": "collapsed_navigation",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "advanced_rendering",
|
||||
"name": "hide_nametag_skins_page",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "native_decorations",
|
||||
"name": "advanced_rendering",
|
||||
"ordinal": 7,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "discord_rpc",
|
||||
"name": "native_decorations",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "developer_mode",
|
||||
"name": "discord_rpc",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "telemetry",
|
||||
"name": "developer_mode",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "personalized_ads",
|
||||
"name": "telemetry",
|
||||
"ordinal": 11,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "onboarded",
|
||||
"name": "personalized_ads",
|
||||
"ordinal": 12,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "extra_launch_args",
|
||||
"name": "onboarded",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "custom_env_vars",
|
||||
"name": "extra_launch_args",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mc_memory_max",
|
||||
"name": "custom_env_vars",
|
||||
"ordinal": 15,
|
||||
"type_info": "Integer"
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mc_force_fullscreen",
|
||||
"name": "mc_memory_max",
|
||||
"ordinal": 16,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "mc_game_resolution_x",
|
||||
"name": "mc_force_fullscreen",
|
||||
"ordinal": 17,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "mc_game_resolution_y",
|
||||
"name": "mc_game_resolution_x",
|
||||
"ordinal": 18,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "hide_on_process_start",
|
||||
"name": "mc_game_resolution_y",
|
||||
"ordinal": 19,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "hook_pre_launch",
|
||||
"name": "hide_on_process_start",
|
||||
"ordinal": 20,
|
||||
"type_info": "Text"
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "hook_wrapper",
|
||||
"name": "hook_pre_launch",
|
||||
"ordinal": 21,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "hook_post_exit",
|
||||
"name": "hook_wrapper",
|
||||
"ordinal": 22,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "custom_dir",
|
||||
"name": "hook_post_exit",
|
||||
"ordinal": 23,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "prev_custom_dir",
|
||||
"name": "custom_dir",
|
||||
"ordinal": 24,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "migrated",
|
||||
"name": "prev_custom_dir",
|
||||
"ordinal": 25,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "migrated",
|
||||
"ordinal": 26,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "feature_flags",
|
||||
"ordinal": 26,
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "toggle_sidebar",
|
||||
"ordinal": 27,
|
||||
"ordinal": 28,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "skipped_update",
|
||||
"ordinal": 28,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "pending_update_toast_for_version",
|
||||
"ordinal": 29,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "auto_download_updates",
|
||||
"name": "pending_update_toast_for_version",
|
||||
"ordinal": 30,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "auto_download_updates",
|
||||
"ordinal": 31,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"ordinal": 31,
|
||||
"ordinal": 32,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
@@ -181,6 +186,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
@@ -202,5 +208,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "07ea3a644644de61c4ed7c30ee711d29fd49f10534230b1b03097275a30cb50f"
|
||||
"hash": "8e62fba05f331f91822ec204695ecb40567541c547181a4ef847318845cf3110"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings\n SET\n max_concurrent_writes = $1,\n max_concurrent_downloads = $2,\n\n theme = $3,\n default_page = $4,\n collapsed_navigation = $5,\n advanced_rendering = $6,\n native_decorations = $7,\n\n discord_rpc = $8,\n developer_mode = $9,\n telemetry = $10,\n personalized_ads = $11,\n\n onboarded = $12,\n\n extra_launch_args = jsonb($13),\n custom_env_vars = jsonb($14),\n mc_memory_max = $15,\n mc_force_fullscreen = $16,\n mc_game_resolution_x = $17,\n mc_game_resolution_y = $18,\n hide_on_process_start = $19,\n\n hook_pre_launch = $20,\n hook_wrapper = $21,\n hook_post_exit = $22,\n\n custom_dir = $23,\n prev_custom_dir = $24,\n migrated = $25,\n\n toggle_sidebar = $26,\n feature_flags = $27,\n hide_nametag_skins_page = $28,\n\n skipped_update = $29,\n pending_update_toast_for_version = $30,\n auto_download_updates = $31,\n\n version = $32\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 32
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a40e60da6dd1312d4a1ed52fa8fd2394e7ad21de1cb44cf8b93c4b1459cdc716"
|
||||
}
|
||||
9
packages/app-lib/COPYING.md
Normal file
9
packages/app-lib/COPYING.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copying
|
||||
|
||||
The source code of Modrinth App's backend is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
||||
|
||||
## Modrinth logo
|
||||
|
||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||
|
||||
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||
@@ -10,7 +10,7 @@ async-compression = { workspace = true, features = ["gzip", "tokio"] }
|
||||
async-recursion = { workspace = true }
|
||||
async-tungstenite = { workspace = true, features = [
|
||||
"tokio-runtime",
|
||||
"tokio-rustls-webpki-roots",
|
||||
"tokio-rustls-webpki-roots"
|
||||
] }
|
||||
async-walkdir = { workspace = true }
|
||||
async_zip = { workspace = true, features = [
|
||||
@@ -46,6 +46,7 @@ itertools = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
notify-debouncer-mini = { workspace = true }
|
||||
p256 = { workspace = true, features = ["ecdsa"] }
|
||||
parking_lot = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
path-util = { workspace = true }
|
||||
phf = { workspace = true }
|
||||
@@ -95,12 +96,7 @@ tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"time",
|
||||
] }
|
||||
tokio-util = { workspace = true, features = [
|
||||
"compat",
|
||||
"io",
|
||||
"io-util",
|
||||
"time",
|
||||
] }
|
||||
tokio-util = { workspace = true, features = ["compat", "io", "io-util", "time"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["chrono", "env-filter"] }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE settings ADD COLUMN locale TEXT NOT NULL DEFAULT 'en-US';
|
||||
@@ -65,6 +65,9 @@ pub enum ErrorKind {
|
||||
#[error("Error fetching URL: {0}")]
|
||||
FetchError(#[from] reqwest::Error),
|
||||
|
||||
#[error("Too many API errors; temporarily blocked")]
|
||||
ApiIsDownError,
|
||||
|
||||
#[error("{0}")]
|
||||
LabrinthError(LabrinthError),
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ impl QuickPlayVersion {
|
||||
let mut singleplayer = QuickPlaySingleplayerVersion::Builtin;
|
||||
let mut singleplayer_version = singleplayer.min_version();
|
||||
|
||||
for version in versions.iter().take(version_index - 1) {
|
||||
for version in versions.iter().take(version_index) {
|
||||
if let Some(check_version) = server_version
|
||||
&& version.id == check_version
|
||||
{
|
||||
|
||||
@@ -26,8 +26,13 @@ pub use event::{
|
||||
pub use logger::start_logger;
|
||||
pub use state::State;
|
||||
|
||||
pub const LAUNCHER_USER_AGENT: &str = concat!(
|
||||
"modrinth/theseus/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (support@modrinth.com)"
|
||||
);
|
||||
pub fn launcher_user_agent() -> String {
|
||||
const LAUNCHER_BASE_USER_AGENT: &str =
|
||||
concat!("modrinth/theseus/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
format!(
|
||||
"{} ({}; support@modrinth.com)",
|
||||
LAUNCHER_BASE_USER_AGENT,
|
||||
std::env::consts::OS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1307,9 +1307,9 @@ impl CachedEntry {
|
||||
let variations =
|
||||
futures::future::try_join_all(filtered_keys.iter().map(
|
||||
|((loaders_key, game_version), hashes)| {
|
||||
fetch_json::<HashMap<String, Version>>(
|
||||
fetch_json::<HashMap<String, Vec<Version>>>(
|
||||
Method::POST,
|
||||
concat!(env!("MODRINTH_API_URL"), "version_files/update"),
|
||||
concat!(env!("MODRINTH_API_URL"), "version_files/update_many"),
|
||||
None,
|
||||
Some(serde_json::json!({
|
||||
"algorithm": "sha1",
|
||||
@@ -1330,28 +1330,30 @@ impl CachedEntry {
|
||||
&filtered_keys[index];
|
||||
|
||||
for hash in hashes {
|
||||
let version = variation.remove(hash);
|
||||
let versions = variation.remove(hash);
|
||||
|
||||
if let Some(version) = version {
|
||||
let version_id = version.id.clone();
|
||||
vals.push((
|
||||
CacheValue::Version(version).get_entry(),
|
||||
false,
|
||||
));
|
||||
if let Some(versions) = versions {
|
||||
for version in versions {
|
||||
let version_id = version.id.clone();
|
||||
vals.push((
|
||||
CacheValue::Version(version).get_entry(),
|
||||
false,
|
||||
));
|
||||
|
||||
vals.push((
|
||||
CacheValue::FileUpdate(CachedFileUpdate {
|
||||
hash: hash.clone(),
|
||||
game_version: game_version.clone(),
|
||||
loaders: loaders_key
|
||||
.split('+')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
update_version_id: version_id,
|
||||
})
|
||||
.get_entry(),
|
||||
true,
|
||||
));
|
||||
vals.push((
|
||||
CacheValue::FileUpdate(CachedFileUpdate {
|
||||
hash: hash.clone(),
|
||||
game_version: game_version.clone(),
|
||||
loaders: loaders_key
|
||||
.split('+')
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
update_version_id: version_id,
|
||||
})
|
||||
.get_entry(),
|
||||
true,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
vals.push((
|
||||
CacheValueType::FileUpdate.get_empty_entry(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::ErrorKind;
|
||||
use crate::LAUNCHER_USER_AGENT;
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::event::emit::emit_friend;
|
||||
@@ -85,7 +84,7 @@ impl FriendsSocket {
|
||||
|
||||
request.headers_mut().insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap(),
|
||||
HeaderValue::from_str(&crate::launcher_user_agent()).unwrap(),
|
||||
);
|
||||
|
||||
let res = connect_async(request).await;
|
||||
|
||||
@@ -1009,17 +1009,15 @@ impl Profile {
|
||||
initial_file.file_name
|
||||
);
|
||||
|
||||
let update_version_id = if let Some(update) = file_updates
|
||||
.iter()
|
||||
.find(|x| x.hash == hash.hash)
|
||||
.map(|x| x.update_version_id.clone())
|
||||
{
|
||||
if let Some(metadata) = &file {
|
||||
if metadata.version_id != update {
|
||||
Some(update)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let update_version_id = if let Some(metadata) = &file {
|
||||
let update_ids: Vec<String> = file_updates
|
||||
.iter()
|
||||
.filter(|x| x.hash == hash.hash)
|
||||
.map(|x| x.update_version_id.clone())
|
||||
.collect();
|
||||
|
||||
if !update_ids.contains(&metadata.version_id) {
|
||||
update_ids.into_iter().next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Settings {
|
||||
pub max_concurrent_writes: usize,
|
||||
|
||||
pub theme: Theme,
|
||||
pub locale: String,
|
||||
pub default_page: DefaultPage,
|
||||
pub collapsed_navigation: bool,
|
||||
pub hide_nametag_skins_page: bool,
|
||||
@@ -66,7 +67,7 @@ impl Settings {
|
||||
"
|
||||
SELECT
|
||||
max_concurrent_writes, max_concurrent_downloads,
|
||||
theme, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,
|
||||
theme, locale, default_page, collapsed_navigation, hide_nametag_skins_page, advanced_rendering, native_decorations,
|
||||
discord_rpc, developer_mode, telemetry, personalized_ads,
|
||||
onboarded,
|
||||
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
|
||||
@@ -85,6 +86,7 @@ impl Settings {
|
||||
max_concurrent_downloads: res.max_concurrent_downloads as usize,
|
||||
max_concurrent_writes: res.max_concurrent_writes as usize,
|
||||
theme: Theme::from_string(&res.theme),
|
||||
locale: res.locale,
|
||||
default_page: DefaultPage::from_string(&res.default_page),
|
||||
collapsed_navigation: res.collapsed_navigation == 1,
|
||||
hide_nametag_skins_page: res.hide_nametag_skins_page == 1,
|
||||
@@ -157,47 +159,49 @@ impl Settings {
|
||||
max_concurrent_downloads = $2,
|
||||
|
||||
theme = $3,
|
||||
default_page = $4,
|
||||
collapsed_navigation = $5,
|
||||
advanced_rendering = $6,
|
||||
native_decorations = $7,
|
||||
locale = $4,
|
||||
default_page = $5,
|
||||
collapsed_navigation = $6,
|
||||
advanced_rendering = $7,
|
||||
native_decorations = $8,
|
||||
|
||||
discord_rpc = $8,
|
||||
developer_mode = $9,
|
||||
telemetry = $10,
|
||||
personalized_ads = $11,
|
||||
discord_rpc = $9,
|
||||
developer_mode = $10,
|
||||
telemetry = $11,
|
||||
personalized_ads = $12,
|
||||
|
||||
onboarded = $12,
|
||||
onboarded = $13,
|
||||
|
||||
extra_launch_args = jsonb($13),
|
||||
custom_env_vars = jsonb($14),
|
||||
mc_memory_max = $15,
|
||||
mc_force_fullscreen = $16,
|
||||
mc_game_resolution_x = $17,
|
||||
mc_game_resolution_y = $18,
|
||||
hide_on_process_start = $19,
|
||||
extra_launch_args = jsonb($14),
|
||||
custom_env_vars = jsonb($15),
|
||||
mc_memory_max = $16,
|
||||
mc_force_fullscreen = $17,
|
||||
mc_game_resolution_x = $18,
|
||||
mc_game_resolution_y = $19,
|
||||
hide_on_process_start = $20,
|
||||
|
||||
hook_pre_launch = $20,
|
||||
hook_wrapper = $21,
|
||||
hook_post_exit = $22,
|
||||
hook_pre_launch = $21,
|
||||
hook_wrapper = $22,
|
||||
hook_post_exit = $23,
|
||||
|
||||
custom_dir = $23,
|
||||
prev_custom_dir = $24,
|
||||
migrated = $25,
|
||||
custom_dir = $24,
|
||||
prev_custom_dir = $25,
|
||||
migrated = $26,
|
||||
|
||||
toggle_sidebar = $26,
|
||||
feature_flags = $27,
|
||||
hide_nametag_skins_page = $28,
|
||||
toggle_sidebar = $27,
|
||||
feature_flags = $28,
|
||||
hide_nametag_skins_page = $29,
|
||||
|
||||
skipped_update = $29,
|
||||
pending_update_toast_for_version = $30,
|
||||
auto_download_updates = $31,
|
||||
skipped_update = $30,
|
||||
pending_update_toast_for_version = $31,
|
||||
auto_download_updates = $32,
|
||||
|
||||
version = $32
|
||||
version = $33
|
||||
",
|
||||
max_concurrent_writes,
|
||||
max_concurrent_downloads,
|
||||
theme,
|
||||
self.locale,
|
||||
default_page,
|
||||
self.collapsed_navigation,
|
||||
self.advanced_rendering,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
//! Functions for fetching information from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::ErrorKind;
|
||||
use crate::LAUNCHER_USER_AGENT;
|
||||
use crate::event::LoadingBarId;
|
||||
use crate::event::emit::emit_loading;
|
||||
use bytes::Bytes;
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use parking_lot::Mutex;
|
||||
use rand::Rng;
|
||||
use reqwest::Method;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
@@ -19,10 +22,120 @@ pub struct IoSemaphore(pub Semaphore);
|
||||
#[derive(Debug)]
|
||||
pub struct FetchSemaphore(pub Semaphore);
|
||||
|
||||
struct FetchFence {
|
||||
inner: Mutex<FenceInner>,
|
||||
}
|
||||
|
||||
impl FetchFence {
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
self.inner.lock().is_blocked()
|
||||
}
|
||||
|
||||
pub fn record_ok(&self) {
|
||||
self.inner.lock().record_ok()
|
||||
}
|
||||
|
||||
pub fn record_fail(&self) {
|
||||
self.inner.lock().record_fail()
|
||||
}
|
||||
}
|
||||
|
||||
struct FenceInner {
|
||||
failures: VecDeque<DateTime<Utc>>,
|
||||
block_until: Option<DateTime<Utc>>,
|
||||
block_factor: i32,
|
||||
}
|
||||
|
||||
impl FenceInner {
|
||||
const FAILURE_WINDOW: TimeDelta = TimeDelta::minutes(3);
|
||||
const FAILURE_THRESHOLD: usize = 4;
|
||||
const BLOCK_DURATION_MIN_BASE: TimeDelta = TimeDelta::minutes(2);
|
||||
const BLOCK_DURATION_MAX_BASE: TimeDelta = TimeDelta::minutes(5);
|
||||
const BLOCK_DURATION_MAX_FACTOR: i32 = 3;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
failures: VecDeque::new(),
|
||||
block_until: None,
|
||||
block_factor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_blocked(&mut self) -> bool {
|
||||
if let Some(until) = self.block_until {
|
||||
if until > Utc::now() {
|
||||
return true;
|
||||
} else {
|
||||
self.block_until = None;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn record_ok(&mut self) {
|
||||
self.prune(Utc::now());
|
||||
}
|
||||
|
||||
pub fn record_fail(&mut self) {
|
||||
self.prune(Utc::now());
|
||||
self.failures.push_back(Utc::now());
|
||||
|
||||
if self.failures.len() >= Self::FAILURE_THRESHOLD {
|
||||
self.trigger_block();
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocks further requests for a random duration between the min and max base durations, scaled by a factor
|
||||
/// of how many blocks have been triggered in this session.
|
||||
///
|
||||
/// As such, for the first block, the duration will be between 2 and 5 minutes.
|
||||
/// - For the second block, between 4 and 10 minutes.
|
||||
/// - For the third block and any further blocks, between 6 and 15 minutes.
|
||||
fn trigger_block(&mut self) {
|
||||
self.block_factor =
|
||||
i32::min(self.block_factor + 1, Self::BLOCK_DURATION_MAX_FACTOR);
|
||||
|
||||
let min = Self::BLOCK_DURATION_MIN_BASE
|
||||
.checked_mul(self.block_factor)
|
||||
.unwrap_or(Self::BLOCK_DURATION_MIN_BASE);
|
||||
let max = Self::BLOCK_DURATION_MAX_BASE
|
||||
.checked_mul(self.block_factor)
|
||||
.unwrap_or(Self::BLOCK_DURATION_MAX_BASE);
|
||||
|
||||
let delta_seconds = (max - min).as_seconds_f64()
|
||||
* rand::thread_rng().gen_range(0.0..=1.0);
|
||||
let duration =
|
||||
min + TimeDelta::milliseconds((delta_seconds * 1000.0) as i64);
|
||||
|
||||
self.block_until = Some(Utc::now() + duration);
|
||||
}
|
||||
|
||||
/// Removes all failure points older than the failure window
|
||||
fn prune(&mut self, now: DateTime<Utc>) {
|
||||
let cutoff = now - Self::FAILURE_WINDOW;
|
||||
|
||||
while let Some(&front) = self.failures.front() {
|
||||
if front < cutoff {
|
||||
self.failures.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GLOBAL_FETCH_FENCE: LazyLock<FetchFence> =
|
||||
LazyLock::new(|| FetchFence {
|
||||
inner: Mutex::new(FenceInner::new()),
|
||||
});
|
||||
|
||||
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
||||
let header =
|
||||
reqwest::header::HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap();
|
||||
reqwest::header::HeaderValue::from_str(&crate::launcher_user_agent())
|
||||
.unwrap();
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||
@@ -30,7 +143,8 @@ pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
.build()
|
||||
.expect("Reqwest Client Building Failed")
|
||||
});
|
||||
const FETCH_ATTEMPTS: usize = 3;
|
||||
|
||||
const FETCH_ATTEMPTS: usize = 2;
|
||||
|
||||
#[tracing::instrument(skip(semaphore))]
|
||||
pub async fn fetch(
|
||||
@@ -78,12 +192,13 @@ pub async fn fetch_advanced(
|
||||
) -> crate::Result<Bytes> {
|
||||
let _permit = semaphore.0.acquire().await?;
|
||||
|
||||
let is_api_url = url.starts_with(env!("MODRINTH_API_URL"))
|
||||
|| url.starts_with(env!("MODRINTH_API_URL_V3"));
|
||||
|
||||
let creds = if header
|
||||
.as_ref()
|
||||
.is_none_or(|x| &*x.0.to_lowercase() != "authorization")
|
||||
&& (url.starts_with("https://cdn.modrinth.com")
|
||||
|| url.starts_with(env!("MODRINTH_API_URL"))
|
||||
|| url.starts_with(env!("MODRINTH_API_URL_V3")))
|
||||
&& (url.starts_with("https://cdn.modrinth.com") || is_api_url)
|
||||
{
|
||||
crate::state::ModrinthCredentials::get_active(exec).await?
|
||||
} else {
|
||||
@@ -91,6 +206,10 @@ pub async fn fetch_advanced(
|
||||
};
|
||||
|
||||
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
|
||||
if is_api_url && GLOBAL_FETCH_FENCE.is_blocked() {
|
||||
return Err(ErrorKind::ApiIsDownError.into());
|
||||
}
|
||||
|
||||
let mut req = REQWEST_CLIENT.request(method.clone(), url);
|
||||
|
||||
if let Some(body) = json_body.clone() {
|
||||
@@ -108,10 +227,16 @@ pub async fn fetch_advanced(
|
||||
let result = req.send().await;
|
||||
match result {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_server_error() && attempt <= FETCH_ATTEMPTS
|
||||
{
|
||||
continue;
|
||||
if resp.status().is_server_error() {
|
||||
if is_api_url {
|
||||
GLOBAL_FETCH_FENCE.record_fail();
|
||||
}
|
||||
|
||||
if attempt <= FETCH_ATTEMPTS {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if resp.status().is_client_error()
|
||||
|| resp.status().is_server_error()
|
||||
{
|
||||
@@ -166,6 +291,11 @@ pub async fn fetch_advanced(
|
||||
}
|
||||
|
||||
tracing::trace!("Done downloading URL {url}");
|
||||
|
||||
if is_api_url {
|
||||
GLOBAL_FETCH_FENCE.record_ok();
|
||||
}
|
||||
|
||||
return Ok(bytes);
|
||||
} else if attempt <= FETCH_ATTEMPTS {
|
||||
continue;
|
||||
@@ -325,3 +455,124 @@ pub async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{TimeDelta, Utc};
|
||||
|
||||
#[test]
|
||||
fn test_fence_block_after_4_fails() {
|
||||
// Update tests if the FenceInner constants change
|
||||
|
||||
let mut fence = FenceInner::new();
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(fence.is_blocked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fence_block_after_4_fails_with_oks() {
|
||||
// Update tests if the FenceInner constants change
|
||||
|
||||
let mut fence = FenceInner::new();
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_ok();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(fence.is_blocked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fence_not_blocked_after_fails_expire() {
|
||||
// Update tests if the FenceInner constants change
|
||||
|
||||
let mut fence = FenceInner::new();
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.prune(Utc::now() + TimeDelta::seconds(60 * 3 + 55)); // Should prune all failures
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(!fence.is_blocked());
|
||||
|
||||
fence.record_fail();
|
||||
assert!(fence.is_blocked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fence_trigger_block_windows() {
|
||||
// brute force flukes
|
||||
for i in 0..128 {
|
||||
let mut fence = FenceInner::new();
|
||||
|
||||
fence.trigger_block();
|
||||
assert!(fence.is_blocked(), "Should be blocked (attempt {i})");
|
||||
|
||||
let block_until = fence.block_until.unwrap();
|
||||
assert!(
|
||||
block_until > Utc::now() + TimeDelta::seconds(60 + 55),
|
||||
"Should be more than 2 minutes (with some leeway) (attempt {i})"
|
||||
); // more than 2 minutes (with some leeway)
|
||||
assert!(
|
||||
block_until < Utc::now() + TimeDelta::seconds(60 * 5),
|
||||
"Should be less than 5 minutes (attempt {i})"
|
||||
); // less than 5 minutes
|
||||
|
||||
fence.block_until = None;
|
||||
|
||||
fence.trigger_block();
|
||||
let block_until = fence.block_until.unwrap();
|
||||
assert!(
|
||||
block_until > Utc::now() + TimeDelta::seconds(60 * 3 + 55),
|
||||
"Should be more than 4 minutes (with some leeway) (attempt {i})"
|
||||
); // more than 4 minutes (with some leeway)
|
||||
assert!(
|
||||
block_until < Utc::now() + TimeDelta::seconds(60 * 10),
|
||||
"Should be less than 10 minutes (attempt {i})"
|
||||
); // less than 10 minutes
|
||||
|
||||
fence.block_until = None;
|
||||
|
||||
fence.trigger_block();
|
||||
let block_until = fence.block_until.unwrap();
|
||||
assert!(
|
||||
block_until > Utc::now() + TimeDelta::seconds(60 * 5 + 55),
|
||||
"Should be more than 6 minutes (with some leeway) (attempt {i})"
|
||||
); // more than 6 minutes (with some leeway)
|
||||
assert!(
|
||||
block_until < Utc::now() + TimeDelta::seconds(60 * 15),
|
||||
"Should be less than 15 minutes (attempt {i})"
|
||||
); // less than 15 minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user