Skip to content

Commit 05aae4a

Browse files
authored
Merge pull request #16 from gorilla-devs/big-refactor
Daedalus overhaul
2 parents 0da957f + 7a0755d commit 05aae4a

45 files changed

Lines changed: 7902 additions & 2447 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,93 @@
1-
RUST_LOG=info
2-
BASE_URL=YOUR_BASE_URL
3-
S3_ACCESS_TOKEN=null
4-
S3_SECRET=null
5-
S3_URL=null
6-
S3_REGION=null
7-
S3_BUCKET_NAME=null
8-
BRAND_NAME=your-brand-name
9-
SUPPORT_EMAIL=support-email
10-
CDN_UPLOAD_DIR=./upload_cdn
11-
SENTRY_DSN=null
1+
# =============================================================================
2+
# Daedalus Client Environment Configuration
3+
# =============================================================================
4+
# Copy this file to .env and fill in your actual values
5+
# Required variables must be set for the application to run
6+
7+
# =============================================================================
8+
# REQUIRED CONFIGURATION
9+
# =============================================================================
10+
11+
# Base URL for CAS objects (public CDN URL where metadata will be served)
12+
BASE_URL=https://cdn.example.com
13+
14+
# Sentry error tracking DSN (get from https://sentry.io)
15+
SENTRY_DSN=https://key@sentry.io/project
16+
17+
# Brand name for metadata files
18+
BRAND_NAME=MyLauncher
19+
20+
# Support email for metadata
21+
SUPPORT_EMAIL=support@example.com
22+
23+
# =============================================================================
24+
# S3 STORAGE CONFIGURATION
25+
# =============================================================================
26+
27+
# S3 bucket name where metadata will be stored
28+
S3_BUCKET_NAME=minecraft-metadata
29+
30+
# S3 region (use "r2" for Cloudflare R2)
31+
# Examples: us-east-1, eu-west-1, ap-southeast-1, r2
32+
S3_REGION=us-east-1
33+
34+
# S3 endpoint URL
35+
# AWS S3: https://s3.amazonaws.com
36+
# Cloudflare R2: https://<account-id>.r2.cloudflarestorage.com
37+
S3_URL=https://s3.amazonaws.com
38+
39+
# S3 access key ID
40+
S3_ACCESS_TOKEN=token
41+
42+
# S3 secret access key
43+
S3_SECRET=secret
44+
45+
# =============================================================================
46+
# OPTIONAL: LOGGING CONFIGURATION
47+
# =============================================================================
48+
49+
# Log output format: "text" or "json"
50+
# Default: text
51+
# LOG_FORMAT=text
52+
53+
# Rust log level filter
54+
# Options: trace, debug, info, warn, error
55+
# Default: info
56+
# RUST_LOG=info
57+
58+
# Betterstack logging token for centralized log management
59+
# Get from https://betterstack.com
60+
# BETTERSTACK_TOKEN=your-betterstack-token
61+
62+
# Betterstack ingestion endpoint URL
63+
# Default: https://in.logs.betterstack.com
64+
# BETTERSTACK_URL=https://in.logs.betterstack.com
65+
66+
# =============================================================================
67+
# OPTIONAL: CLOUDFLARE INTEGRATION
68+
# =============================================================================
69+
70+
# Enable Cloudflare cache purging on updates
71+
# Default: false
72+
# CLOUDFLARE_INTEGRATION=true
73+
74+
# Cloudflare API token (required if CLOUDFLARE_INTEGRATION=true)
75+
# Create at: https://dash.cloudflare.com/profile/api-tokens
76+
# Required permissions: Zone.Cache Purge
77+
# CLOUDFLARE_TOKEN=your-cloudflare-token
78+
79+
# Cloudflare zone ID (required if CLOUDFLARE_INTEGRATION=true)
80+
# Find in: Zone Overview > API section
81+
# CLOUDFLARE_ZONE_ID=your-zone-id
82+
83+
# =============================================================================
84+
# OPTIONAL: ADVANCED CONFIGURATION
85+
# =============================================================================
86+
87+
# Local directory for CDN file uploads
88+
# Default: ./upload_cdn
89+
# CDN_UPLOAD_DIR=./upload_cdn
90+
91+
# Force reprocessing of all NeoForge versions (useful for debugging)
92+
# Default: false
93+
# FORCE_REPROCESS=false

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[workspace]
2+
resolver = "2"
23

34
members = [
45
"daedalus",

daedalus/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
name = "daedalus"
3-
version = "0.1.21"
3+
version = "5.0.0"
44
authors = ["Jai A <jaiagr+gpg@pm.me>"]
5-
edition = "2021"
5+
edition = "2024"
66
license = "MIT"
77
description = "Utilities for querying and parsing Minecraft metadata"
88
repository = "https://github.com/modrinth/daedalus/"
@@ -26,7 +26,6 @@ bytes = "1"
2626
thiserror = "1"
2727
tokio = { version = "1", features = ["full"] }
2828
sha1 = { version = "0.6.1", features = ["std"] }
29-
bincode = { version = "2.0.0-rc.3", features = ["serde"], optional = true }
3029
once_cell = "1"
3130
url = "2"
3231
lenient_semver = "0"

daedalus/src/lib.rs

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use std::{
88
cmp::Ordering, convert::TryFrom, fmt::Display, path::PathBuf, str::FromStr,
9-
time::Duration,
9+
sync::LazyLock, time::Duration,
1010
};
1111

1212
use backon::{ExponentialBuilder, Retryable};
@@ -17,6 +17,18 @@ use serde::{Deserialize, Serialize};
1717
pub mod minecraft;
1818
/// Models and methods for fetching metadata for Minecraft mod loaders
1919
pub mod modded;
20+
/// Custom version comparison for Minecraft versions
21+
pub mod version;
22+
23+
/// HTTP client configuration constants
24+
/// TCP keepalive interval for persistent connections
25+
const TCP_KEEPALIVE_SECS: u64 = 10;
26+
/// Overall request timeout including reading response
27+
const REQUEST_TIMEOUT_SECS: u64 = 120;
28+
/// Connection establishment timeout
29+
const CONNECT_TIMEOUT_SECS: u64 = 30;
30+
/// Maximum idle connections per host in the pool
31+
const MAX_IDLE_CONNECTIONS_PER_HOST: usize = 10;
2032

2133
/// Your branding, used for the user agent and similar
2234
#[derive(Debug)]
@@ -30,6 +42,30 @@ pub struct Branding {
3042
/// The branding of your application
3143
pub static BRANDING: OnceCell<Branding> = OnceCell::new();
3244

45+
/// Global HTTP client with connection pooling and TCP keepalive
46+
///
47+
/// # Panics
48+
/// Panics if the HTTP client fails to initialize. This is intentional as
49+
/// the application cannot function without a working HTTP client (e.g., if
50+
/// TLS initialization fails, which is extremely rare on modern systems).
51+
static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
52+
let mut headers = reqwest::header::HeaderMap::new();
53+
if let Ok(header) = reqwest::header::HeaderValue::from_str(
54+
&BRANDING.get_or_init(Branding::default).header_value,
55+
) {
56+
headers.insert(reqwest::header::USER_AGENT, header);
57+
}
58+
59+
reqwest::Client::builder()
60+
.tcp_keepalive(Some(Duration::from_secs(TCP_KEEPALIVE_SECS)))
61+
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
62+
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
63+
.default_headers(headers)
64+
.pool_max_idle_per_host(MAX_IDLE_CONNECTIONS_PER_HOST)
65+
.build()
66+
.expect("Failed to create HTTP client")
67+
});
68+
3369
impl Branding {
3470
/// Creates a new branding instance
3571
pub fn new(name: String, email: String) -> Branding {
@@ -100,7 +136,6 @@ pub enum Error {
100136
MirrorsFailed(String),
101137
}
102138

103-
#[cfg_attr(feature = "bincode", derive(Encode, Decode))]
104139
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Default)]
105140
/// A specifier string for Gradle
106141
pub struct GradleSpecifier {
@@ -166,12 +201,10 @@ impl GradleSpecifier {
166201

167202
/// Returns if specifier belongs to a lwjgl library
168203
pub fn is_lwjgl(&self) -> bool {
169-
vec![
170-
"org.lwjgl",
204+
["org.lwjgl",
171205
"org.lwjgl.lwjgl",
172206
"net.java.jinput",
173-
"net.java.jutils",
174-
]
207+
"net.java.jutils"]
175208
.contains(&self.package.as_str())
176209
}
177210

@@ -186,7 +219,7 @@ impl GradleSpecifier {
186219
"{}:{}:{}",
187220
self.package,
188221
self.artifact,
189-
self.identifier.clone().unwrap_or("".to_string())
222+
self.identifier.as_deref().unwrap_or("")
190223
)
191224
}
192225

@@ -195,17 +228,12 @@ impl GradleSpecifier {
195228
/// Returns Ordering::Greater if self is greater than other
196229
/// Returns Ordering::Less if self is less than other
197230
pub fn compare_versions(&self, other: &Self) -> Result<Ordering, Error> {
198-
let x = lenient_semver::parse(self.version.as_str());
199-
let y = lenient_semver::parse(other.version.as_str());
231+
let x = lenient_semver::parse(self.version.as_str())
232+
.map_err(|_| Error::ParseError("Unable to parse version".to_string()))?;
233+
let y = lenient_semver::parse(other.version.as_str())
234+
.map_err(|_| Error::ParseError("Unable to parse version".to_string()))?;
200235

201-
if x.is_err() || y.is_err() {
202-
return Err(Error::ParseError(
203-
"Unable to parse version".to_string(),
204-
));
205-
}
206-
207-
// safe to unwrap because we already checked for errors
208-
Ok(x.unwrap().cmp(&y.unwrap()))
236+
Ok(x.cmp(&y))
209237
}
210238
}
211239

@@ -358,32 +386,16 @@ pub async fn download_file_mirrors(
358386
}
359387
}
360388

361-
return Err(Error::MirrorsFailed("No mirrors succeeded!".to_string()));
389+
Err(Error::MirrorsFailed("No mirrors succeeded!".to_string()))
362390
}
363391

364392
/// Downloads a file with retry and checksum functionality
365393
pub async fn download_file(
366394
url: &str,
367395
sha1: Option<&str>,
368396
) -> Result<bytes::Bytes, Error> {
369-
let mut headers = reqwest::header::HeaderMap::new();
370-
if let Ok(header) = reqwest::header::HeaderValue::from_str(
371-
&BRANDING.get_or_init(Branding::default).header_value,
372-
) {
373-
headers.insert(reqwest::header::USER_AGENT, header);
374-
}
375-
let client = reqwest::Client::builder()
376-
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
377-
.timeout(std::time::Duration::from_secs(15))
378-
.default_headers(headers)
379-
.build()
380-
.map_err(|err| Error::FetchError {
381-
inner: err,
382-
item: url.to_string(),
383-
})?;
384-
385397
(|| async {
386-
let result = client.get(url).send().await;
398+
let result = HTTP_CLIENT.get(url).send().await;
387399

388400
match result {
389401
Ok(x) => {

0 commit comments

Comments
 (0)