Skip to content

Commit c312c42

Browse files
committed
added Arch4edu support
1 parent 5d26931 commit c312c42

24 files changed

+237
-96
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 0.27.0 (2026-02-08)
2+
3+
- added Arch4edu support (`arch4edu`)
4+
15
# 0.26.0 (2026-02-08)
26

37
- added `--disable-untested-fallback` to exit with error when all speed tests fail

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rate_mirrors"
3-
version = "0.26.0"
3+
version = "0.27.0"
44
authors = ["Nikita Almakov <nikita.almakov@gmail.com>"]
55
edition = "2024"
66
description = "Everyday-use client-side map-aware mirror ranking tool (Arch Linux; Manjaro; custom ones)"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ rate-mirrors --help
5454
| Command | Distribution | Notes |
5555
|---------|-------------|-------|
5656
| `rate-mirrors arch` | Arch Linux | Skips outdated/syncing mirrors |
57+
| `rate-mirrors arch4edu` | Arch4edu | |
5758
| `rate-mirrors archarm` | Arch Linux ARM | |
5859
| `rate-mirrors arcolinux` | ArcoLinux | |
5960
| `rate-mirrors artix` | Artix Linux | |

src/config.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::mirror::Mirror;
2+
use crate::target_configs::arch4edu::Arch4eduTarget;
23
use crate::target_configs::archarm::ArcharmTarget;
34
use crate::target_configs::archlinux::ArchTarget;
45
use crate::target_configs::archlinuxcn::ArchCNTarget;
@@ -14,7 +15,7 @@ use crate::target_configs::openbsd::OpenBSDTarget;
1415
use crate::target_configs::rebornos::RebornOSTarget;
1516
use crate::target_configs::stdin::StdinTarget;
1617
// use crate::target_configs::ubuntu::UbuntuTarget;
17-
use ambassador::{Delegate, delegatable_trait};
18+
use ambassador::{delegatable_trait, Delegate};
1819
use clap::{Parser, Subcommand};
1920
use serde::de::DeserializeOwned;
2021
use std::collections::HashSet;
@@ -89,10 +90,7 @@ pub trait LogFormatter {
8990

9091
#[delegatable_trait]
9192
pub trait FetchMirrors {
92-
fn fetch_mirrors(
93-
&self,
94-
tx_progress: mpsc::Sender<String>,
95-
) -> Result<Vec<Mirror>, AppError>;
93+
fn fetch_mirrors(&self, tx_progress: mpsc::Sender<String>) -> Result<Vec<Mirror>, AppError>;
9694
}
9795

9896
#[derive(Debug, Subcommand, Clone, Delegate)]
@@ -105,6 +103,10 @@ pub enum Target {
105103
/// test archlinux mirrors
106104
Arch(ArchTarget),
107105

106+
/// test arch4edu mirrors
107+
#[command(name = "arch4edu")]
108+
Arch4edu(Arch4eduTarget),
109+
108110
/// test archlinuxcn mirrors
109111
#[command(name = "archlinuxcn")]
110112
ArchCN(ArchCNTarget),
@@ -335,7 +337,6 @@ impl Config {
335337
.unwrap_or(false)
336338
}
337339
}
338-
339340
}
340341

341342
fn convert_reqwest_error(e: reqwest::Error, url: &str) -> AppError {

src/main.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,7 @@ fn main() -> Result<(), AppError> {
119119
let (tx_mirrors, rx_mirrors) = mpsc::channel::<Mirror>();
120120

121121
let thread_handle = thread::spawn(move || -> Result<(), AppError> {
122-
let mut mirrors = config
123-
.target
124-
.fetch_mirrors(tx_progress.clone())?;
122+
let mut mirrors = config.target.fetch_mirrors(tx_progress.clone())?;
125123

126124
// Centralized protocol filtering
127125
let before_protocol = mirrors.len();

src/speed_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::convert::From;
1313
use std::fmt;
1414
use std::fmt::Debug;
1515
use std::sync::mpsc::Sender;
16-
use std::sync::{Arc, mpsc};
16+
use std::sync::{mpsc, Arc};
1717
use std::time::{Duration, Instant};
1818

1919
use tokio::runtime::Runtime;

src/target_configs/arch4edu.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use clap::Args;
2+
3+
#[derive(Debug, Clone, Args)]
4+
pub struct Arch4eduTarget {
5+
/// Fetch list of mirrors timeout in milliseconds
6+
#[arg(
7+
env = "RATE_MIRRORS_FETCH_MIRRORS_TIMEOUT",
8+
long,
9+
default_value = "15000"
10+
)]
11+
pub fetch_mirrors_timeout: u64,
12+
13+
/// Path to be joined to a mirror url and used for speed testing
14+
/// the file should be big enough to allow for testing high
15+
/// speed connections
16+
#[arg(
17+
env = "RATE_MIRRORS_PATH_TO_TEST",
18+
long,
19+
default_value = "x86_64/arch4edu.files",
20+
verbatim_doc_comment
21+
)]
22+
pub path_to_test: String,
23+
24+
/// Architecture
25+
#[arg(env = "RATE_MIRRORS_ARCH", long, default_value = "auto")]
26+
pub arch: String,
27+
28+
/// comment prefix to use when outputting
29+
#[arg(env = "RATE_MIRRORS_COMMENT_PREFIX", long, default_value = "# ")]
30+
pub comment_prefix: String,
31+
}

src/target_configs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod arch4edu;
12
pub mod archarm;
23
pub mod archlinux;
34
pub mod archlinuxcn;

src/targets/arch4edu.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use crate::config::{fetch_text, AppError, FetchMirrors, LogFormatter};
2+
use crate::mirror::Mirror;
3+
use crate::target_configs::arch4edu::Arch4eduTarget;
4+
use std::fmt::Display;
5+
use std::sync::mpsc;
6+
use url::Url;
7+
8+
const ARCH4EDU_MIRRORLIST_URL: &str =
9+
"https://raw.githubusercontent.com/arch4edu/mirrorlist/refs/heads/master/mirrorlist.arch4edu";
10+
11+
fn parse_mirror_url(line: &str) -> Option<Url> {
12+
let mut cleaned = line.trim_start();
13+
while cleaned.starts_with('#') {
14+
cleaned = cleaned.trim_start_matches('#').trim_start();
15+
}
16+
17+
let raw_url = cleaned
18+
.strip_prefix("Global Server = ")
19+
.or_else(|| cleaned.strip_prefix("Server = "))?
20+
.trim()
21+
.replace("$arch", "");
22+
23+
if raw_url.is_empty() {
24+
return None;
25+
}
26+
27+
Url::parse(&raw_url).ok()
28+
}
29+
30+
impl LogFormatter for Arch4eduTarget {
31+
fn format_comment(&self, message: impl Display) -> String {
32+
format!("{}{}", self.comment_prefix, message)
33+
}
34+
35+
fn format_mirror(&self, mirror: &Mirror) -> String {
36+
let arch = if self.arch == "auto" {
37+
"$arch"
38+
} else {
39+
&self.arch
40+
};
41+
42+
format!("Server = {}{}", mirror.url, arch)
43+
}
44+
}
45+
46+
impl FetchMirrors for Arch4eduTarget {
47+
fn fetch_mirrors(&self, _tx_progress: mpsc::Sender<String>) -> Result<Vec<Mirror>, AppError> {
48+
let output = fetch_text(ARCH4EDU_MIRRORLIST_URL, self.fetch_mirrors_timeout)?;
49+
50+
let mirrors = output
51+
.lines()
52+
.filter_map(parse_mirror_url)
53+
.map(|url| {
54+
let url_to_test = url
55+
.join(&self.path_to_test)
56+
.expect("failed to join path_to_test");
57+
Mirror {
58+
country: None,
59+
url,
60+
url_to_test,
61+
}
62+
})
63+
.collect();
64+
65+
Ok(mirrors)
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use super::parse_mirror_url;
72+
use crate::config::LogFormatter;
73+
use crate::mirror::Mirror;
74+
use crate::target_configs::arch4edu::Arch4eduTarget;
75+
use url::Url;
76+
77+
#[test]
78+
fn parse_server_line() {
79+
let url = parse_mirror_url("#Server = https://mirror.example/arch4edu/$arch").unwrap();
80+
assert_eq!(url.as_str(), "https://mirror.example/arch4edu/");
81+
}
82+
83+
#[test]
84+
fn parse_global_server_line() {
85+
let url =
86+
parse_mirror_url("## Global Server = https://mirror.example/arch4edu/$arch").unwrap();
87+
assert_eq!(url.as_str(), "https://mirror.example/arch4edu/");
88+
}
89+
90+
#[test]
91+
fn ignore_non_server_lines() {
92+
assert!(parse_mirror_url("# random comment").is_none());
93+
assert!(parse_mirror_url("Include = /etc/pacman.d/mirrorlist").is_none());
94+
}
95+
96+
#[test]
97+
fn format_mirror_uses_arch_placeholder_for_auto() {
98+
let target = Arch4eduTarget {
99+
fetch_mirrors_timeout: 15_000,
100+
path_to_test: "arch4edu/x86_64/arch4edu.files".to_string(),
101+
arch: "auto".to_string(),
102+
comment_prefix: "# ".to_string(),
103+
};
104+
let mirror = Mirror {
105+
country: None,
106+
url: Url::parse("https://mirror.example/arch4edu/").unwrap(),
107+
url_to_test: Url::parse("https://mirror.example/arch4edu/x86_64/arch4edu.files")
108+
.unwrap(),
109+
};
110+
111+
assert_eq!(
112+
target.format_mirror(&mirror),
113+
"Server = https://mirror.example/arch4edu/$arch"
114+
);
115+
}
116+
117+
#[test]
118+
fn format_mirror_uses_custom_arch() {
119+
let target = Arch4eduTarget {
120+
fetch_mirrors_timeout: 15_000,
121+
path_to_test: "arch4edu/x86_64/arch4edu.files".to_string(),
122+
arch: "x86_64".to_string(),
123+
comment_prefix: "# ".to_string(),
124+
};
125+
let mirror = Mirror {
126+
country: None,
127+
url: Url::parse("https://mirror.example/arch4edu/").unwrap(),
128+
url_to_test: Url::parse("https://mirror.example/arch4edu/x86_64/arch4edu.files")
129+
.unwrap(),
130+
};
131+
132+
assert_eq!(
133+
target.format_mirror(&mirror),
134+
"Server = https://mirror.example/arch4edu/x86_64"
135+
);
136+
}
137+
}

0 commit comments

Comments
 (0)