use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
const ACCESS_TOKEN: &str = "YOUR_CANVAS_API_TOKEN";
const BASE_URL: &str = "https://unt.instructure.com/api/v1/files";
const CENTER_ID: u64 = 37826490;
const RADIUS: u64 = 200000;
const NUM_WORKERS: usize = 20;
const RATE_LIMIT_PAUSE_THRESHOLD: f64 = 50.0;
const RATE_LIMIT_RESUME_THRESHOLD: f64 = 200.0;
const RATE_LIMIT_SLEEP_MS: u64 = 2000;
fn parse_rate_limit(resp: &reqwest::blocking::Response) -> Option<f64> {
resp.headers()
.get("x-rate-limit-remaining")?
.to_str()
.ok()?
.parse::<f64>()
.ok()
}
fn main() {
let start = CENTER_ID - RADIUS;
let end = CENTER_ID + RADIUS;
let ids: Vec<u64> = (start..=end).collect();
let total = ids.len() as u64;
let pb = ProgressBar::new(total);
pb.set_style(
ProgressStyle::with_template(
"[{percent:>3}%] {spinner:.cyan} | {pos}/{len} IDs | elapsed {elapsed_precise} | ETA {eta_precise} | RL: {msg} {bar:25.green/white}",
)
.unwrap()
.progress_chars("█▉▊▋▌▍▎▏ ")
.tick_chars("|/-\\"),
);
pb.set_message("---");
pb.enable_steady_tick(Duration::from_millis(100));
let ids = Arc::new(Mutex::new(ids.into_iter()));
let pb = Arc::new(pb);
let found: Arc<Mutex<Vec<(u64, String)>>> = Arc::new(Mutex::new(Vec::new()));
let rate_limit_remaining: Arc<Mutex<f64>> = Arc::new(Mutex::new(f64::MAX));
let mut handles = vec![];
for _ in 0..NUM_WORKERS {
let ids = Arc::clone(&ids);
let pb = Arc::clone(&pb);
let found = Arc::clone(&found);
let rl = Arc::clone(&rate_limit_remaining);
let handle = thread::spawn(move || {
let client = Client::builder()
.timeout(Duration::from_secs(15))
.build()
.expect("Failed to build HTTP client");
loop {
loop {
let remaining = *rl.lock().unwrap();
if remaining > RATE_LIMIT_PAUSE_THRESHOLD {
break;
}
pb.set_message(format!("PAUSED ({:.0} left)", remaining));
thread::sleep(Duration::from_millis(RATE_LIMIT_SLEEP_MS));
let mut r = rl.lock().unwrap();
if *r <= RATE_LIMIT_PAUSE_THRESHOLD {
*r = RATE_LIMIT_RESUME_THRESHOLD;
}
}
let id = {
let mut iter = ids.lock().unwrap();
iter.next()
};
let id = match id {
Some(id) => id,
None => break,
};
let url = format!("{}/{}?access_token={}", BASE_URL, id, ACCESS_TOKEN);
let head_resp = client.head(&url).send();
if let Ok(resp) = head_resp {
if let Some(remaining) = parse_rate_limit(&resp) {
let mut r = rl.lock().unwrap();
*r = remaining;
pb.set_message(format!("{:.0}", remaining));
}
if resp.status().is_success() {
if let Ok(get_resp) = client.get(&url).send() {
if let Some(remaining) = parse_rate_limit(&get_resp) {
let mut r = rl.lock().unwrap();
*r = remaining;
pb.set_message(format!("{:.0}", remaining));
}
if let Ok(json) = get_resp.json::<serde_json::Value>() {
let name = json["display_name"]
.as_str()
.unwrap_or("<unknown>")
.to_string();
pb.println(format!("FOUND: {}. Name: {}", id, name));
found.lock().unwrap().push((id, name));
}
}
}
}
pb.inc(1);
}
});
handles.push(handle);
}
for h in handles {
h.join().expect("Worker thread panicked");
}
pb.finish_with_message("done");
let found = found.lock().unwrap();
println!("\n=== Results ({} found) ===", found.len());
for (id, name) in found.iter() {
println!(" ID {}: {}", id, name);
}
}