Skip to content

Commit 8a12dcb

Browse files
Caching
1 parent 2c40188 commit 8a12dcb

5 files changed

Lines changed: 382 additions & 80 deletions

File tree

src/cache.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use reqwest::Client;
2+
use std::collections::HashMap;
3+
use std::sync::Arc;
4+
use std::time::{Duration, Instant};
5+
use tokio::sync::RwLock;
6+
7+
#[derive(Clone)]
8+
pub struct CachedClient {
9+
pub client: Client,
10+
pub expires_at: Instant,
11+
}
12+
13+
#[derive(Clone)]
14+
pub struct CachedData {
15+
pub data: String,
16+
pub expires_at: Instant,
17+
}
18+
19+
pub struct Cache {
20+
clients: Arc<RwLock<HashMap<String, CachedClient>>>,
21+
pages: Arc<RwLock<HashMap<String, CachedData>>>,
22+
client_ttl: Duration,
23+
page_ttl: Duration,
24+
}
25+
26+
impl Cache {
27+
pub fn new(client_ttl_secs: u64, page_ttl_secs: u64) -> Self {
28+
Self {
29+
clients: Arc::new(RwLock::new(HashMap::new())),
30+
pages: Arc::new(RwLock::new(HashMap::new())),
31+
client_ttl: Duration::from_secs(client_ttl_secs),
32+
page_ttl: Duration::from_secs(page_ttl_secs),
33+
}
34+
}
35+
36+
fn make_client_key(username: &str, url: &str) -> String {
37+
format!("{}:{}", username, url)
38+
}
39+
40+
fn make_page_key(username: &str, url: &str, endpoint: &str, params: &str) -> String {
41+
format!("{}:{}:{}:{}", username, url, endpoint, params)
42+
}
43+
44+
pub async fn get_client(&self, username: &str, url: &str) -> Option<Client> {
45+
let key = Self::make_client_key(username, url);
46+
let clients = self.clients.read().await;
47+
48+
if let Some(cached) = clients.get(&key) {
49+
if Instant::now() < cached.expires_at {
50+
return Some(cached.client.clone());
51+
}
52+
}
53+
None
54+
}
55+
56+
pub async fn set_client(&self, username: &str, url: &str, client: Client) {
57+
let key = Self::make_client_key(username, url);
58+
let cached = CachedClient {
59+
client,
60+
expires_at: Instant::now() + self.client_ttl,
61+
};
62+
63+
let mut clients = self.clients.write().await;
64+
clients.insert(key, cached);
65+
}
66+
67+
pub async fn get_page(
68+
&self,
69+
username: &str,
70+
url: &str,
71+
endpoint: &str,
72+
params: &str,
73+
) -> Option<String> {
74+
let key = Self::make_page_key(username, url, endpoint, params);
75+
let pages = self.pages.read().await;
76+
77+
if let Some(cached) = pages.get(&key) {
78+
if Instant::now() < cached.expires_at {
79+
return Some(cached.data.clone());
80+
}
81+
}
82+
None
83+
}
84+
85+
pub async fn set_page(
86+
&self,
87+
username: &str,
88+
url: &str,
89+
endpoint: &str,
90+
params: &str,
91+
data: String,
92+
) {
93+
let key = Self::make_page_key(username, url, endpoint, params);
94+
let cached = CachedData {
95+
data,
96+
expires_at: Instant::now() + self.page_ttl,
97+
};
98+
99+
let mut pages = self.pages.write().await;
100+
pages.insert(key, cached);
101+
}
102+
103+
pub async fn clear_expired(&self) {
104+
let now = Instant::now();
105+
106+
let mut clients = self.clients.write().await;
107+
clients.retain(|_, v| now < v.expires_at);
108+
109+
let mut pages = self.pages.write().await;
110+
pages.retain(|_, v| now < v.expires_at);
111+
}
112+
113+
}
114+
115+
impl Clone for Cache {
116+
fn clone(&self) -> Self {
117+
Self {
118+
clients: Arc::clone(&self.clients),
119+
pages: Arc::clone(&self.pages),
120+
client_ttl: self.client_ttl,
121+
page_ttl: self.page_ttl,
122+
}
123+
}
124+
}

src/fetchers.rs

Lines changed: 174 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use chrono::Datelike;
33
use chrono::Utc;
44
use scraper::{Html, Selector};
55
use std::collections::HashMap;
6+
use crate::cache::Cache;
67

78
async fn fetch_page(
89
client: &Client,
@@ -25,27 +26,129 @@ async fn fetch_page(
2526
Ok(body)
2627
}
2728

28-
pub async fn fetch_info_page(client: &Client, base_url: &str) -> Result<String, String> {
29-
fetch_page(client, base_url, "Registration.aspx").await
29+
pub async fn fetch_info_page(
30+
client: &Client,
31+
base_url: &str,
32+
cache: &Cache,
33+
username: &str,
34+
no_cache: bool,
35+
) -> Result<String, String> {
36+
if !no_cache {
37+
if let Some(cached) = cache.get_page(username, base_url, "Registration.aspx", "").await {
38+
return Ok(cached);
39+
}
40+
}
41+
42+
let html = fetch_page(client, base_url, "Registration.aspx").await?;
43+
44+
if !no_cache {
45+
cache.set_page(username, base_url, "Registration.aspx", "", html.clone()).await;
46+
}
47+
48+
Ok(html)
3049
}
3150

32-
pub async fn fetch_assignments_page(client: &Client, base_url: &str) -> Result<String, String> {
33-
fetch_page(client, base_url, "Assignments.aspx").await
51+
pub async fn fetch_assignments_page(
52+
client: &Client,
53+
base_url: &str,
54+
cache: &Cache,
55+
username: &str,
56+
no_cache: bool,
57+
) -> Result<String, String> {
58+
if !no_cache {
59+
if let Some(cached) = cache.get_page(username, base_url, "Assignments.aspx", "current").await {
60+
return Ok(cached);
61+
}
62+
}
63+
64+
let html = fetch_page(client, base_url, "Assignments.aspx").await?;
65+
66+
if !no_cache {
67+
cache.set_page(username, base_url, "Assignments.aspx", "current", html.clone()).await;
68+
}
69+
70+
Ok(html)
3471
}
3572

36-
pub async fn fetch_report_page(client: &Client, base_url: &str) -> Result<String, String> {
37-
fetch_page(client, base_url, "ReportCards.aspx").await
73+
pub async fn fetch_report_page(
74+
client: &Client,
75+
base_url: &str,
76+
cache: &Cache,
77+
username: &str,
78+
no_cache: bool,
79+
) -> Result<String, String> {
80+
if !no_cache {
81+
if let Some(cached) = cache.get_page(username, base_url, "ReportCards.aspx", "").await {
82+
return Ok(cached);
83+
}
84+
}
85+
86+
let html = fetch_page(client, base_url, "ReportCards.aspx").await?;
87+
88+
if !no_cache {
89+
cache.set_page(username, base_url, "ReportCards.aspx", "", html.clone()).await;
90+
}
91+
92+
Ok(html)
3893
}
3994

40-
pub async fn fetch_progress_page(client: &Client, base_url: &str) -> Result<String, String> {
41-
fetch_page(client, base_url, "InterimProgress.aspx").await
95+
pub async fn fetch_progress_page(
96+
client: &Client,
97+
base_url: &str,
98+
cache: &Cache,
99+
username: &str,
100+
no_cache: bool,
101+
) -> Result<String, String> {
102+
if !no_cache {
103+
if let Some(cached) = cache.get_page(username, base_url, "InterimProgress.aspx", "").await {
104+
return Ok(cached);
105+
}
106+
}
107+
108+
let html = fetch_page(client, base_url, "InterimProgress.aspx").await?;
109+
110+
if !no_cache {
111+
cache.set_page(username, base_url, "InterimProgress.aspx", "", html.clone()).await;
112+
}
113+
114+
Ok(html)
42115
}
43116

44-
pub async fn fetch_transcript_page(client: &Client, base_url: &str) -> Result<String, String> {
45-
fetch_page(client, base_url, "Transcript.aspx").await
117+
pub async fn fetch_transcript_page(
118+
client: &Client,
119+
base_url: &str,
120+
cache: &Cache,
121+
username: &str,
122+
no_cache: bool,
123+
) -> Result<String, String> {
124+
if !no_cache {
125+
if let Some(cached) = cache.get_page(username, base_url, "Transcript.aspx", "").await {
126+
return Ok(cached);
127+
}
128+
}
129+
130+
let html = fetch_page(client, base_url, "Transcript.aspx").await?;
131+
132+
if !no_cache {
133+
cache.set_page(username, base_url, "Transcript.aspx", "", html.clone()).await;
134+
}
135+
136+
Ok(html)
46137
}
47138

48-
pub async fn fetch_name_page(client: &Client, base_url: &str) -> Result<String, String> {
139+
pub async fn fetch_name_page(
140+
client: &Client,
141+
base_url: &str,
142+
cache: &Cache,
143+
username: &str,
144+
no_cache: bool,
145+
) -> Result<String, String> {
146+
if !no_cache {
147+
if let Some(cached) = cache.get_page(username, base_url, "Classwork", "").await {
148+
return Ok(cached);
149+
}
150+
}
151+
49152
let url = format!("{}/HomeAccess/Classes/Classwork", base_url);
50153

51154
let response = client
@@ -54,12 +157,16 @@ pub async fn fetch_name_page(client: &Client, base_url: &str) -> Result<String,
54157
.await
55158
.map_err(|_| "Failed to fetch classwork page".to_string())?;
56159

57-
let body = response
160+
let html = response
58161
.text()
59162
.await
60163
.map_err(|_| "Failed to read classwork page body".to_string())?;
61164

62-
Ok(body)
165+
if !no_cache {
166+
cache.set_page(username, base_url, "Classwork", "", html.clone()).await;
167+
}
168+
169+
Ok(html)
63170
}
64171

65172
fn format_six_weeks_param(input: &str) -> String {
@@ -102,44 +209,7 @@ pub async fn fetch_assignments_page_for_six_weeks(
102209
.await
103210
.map_err(|_| "Failed to read assignments page".to_string())?;
104211

105-
let payload = {
106-
let document = Html::parse_document(&body);
107-
108-
let viewstate = document
109-
.select(&Selector::parse("input[name='__VIEWSTATE']").unwrap())
110-
.next()
111-
.and_then(|el| el.value().attr("value"))
112-
.unwrap_or("")
113-
.to_string();
114-
115-
let generator = document
116-
.select(&Selector::parse("input[name='__VIEWSTATEGENERATOR']").unwrap())
117-
.next()
118-
.and_then(|el| el.value().attr("value"))
119-
.unwrap_or("")
120-
.to_string();
121-
122-
let validation = document
123-
.select(&Selector::parse("input[name='__EVENTVALIDATION']").unwrap())
124-
.next()
125-
.and_then(|el| el.value().attr("value"))
126-
.unwrap_or("")
127-
.to_string();
128-
129-
let mut form_data: HashMap<&str, String> = HashMap::new();
130-
form_data.insert("__EVENTTARGET", "ctl00$plnMain$btnRefreshView".to_string());
131-
form_data.insert("__EVENTARGUMENT", "".to_string());
132-
form_data.insert("__LASTFOCUS", "".to_string());
133-
form_data.insert("__VIEWSTATE", viewstate);
134-
form_data.insert("__VIEWSTATEGENERATOR", generator);
135-
form_data.insert("__EVENTVALIDATION", validation);
136-
form_data.insert("ctl00$plnMain$ddlReportCardRuns", adjusted_six_weeks);
137-
form_data.insert("ctl00$plnMain$ddlClasses", "ALL".to_string());
138-
form_data.insert("ctl00$plnMain$ddlCompetencies", "ALL".to_string());
139-
form_data.insert("ctl00$plnMain$ddlOrderBy", "Class".to_string());
140-
141-
form_data
142-
};
212+
let payload = extract_form_data(&body, &adjusted_six_weeks);
143213

144214
let post_resp = client
145215
.post(&assignments_url)
@@ -155,3 +225,56 @@ pub async fn fetch_assignments_page_for_six_weeks(
155225

156226
Ok(post_body)
157227
}
228+
229+
fn extract_form_data(body: &str, adjusted_six_weeks: &str) -> HashMap<&'static str, String> {
230+
let document = Html::parse_document(body);
231+
232+
static VIEWSTATE_SEL: std::sync::OnceLock<Selector> = std::sync::OnceLock::new();
233+
static GENERATOR_SEL: std::sync::OnceLock<Selector> = std::sync::OnceLock::new();
234+
static VALIDATION_SEL: std::sync::OnceLock<Selector> = std::sync::OnceLock::new();
235+
236+
let viewstate_sel = VIEWSTATE_SEL.get_or_init(|| {
237+
Selector::parse("input[name='__VIEWSTATE']").unwrap()
238+
});
239+
let generator_sel = GENERATOR_SEL.get_or_init(|| {
240+
Selector::parse("input[name='__VIEWSTATEGENERATOR']").unwrap()
241+
});
242+
let validation_sel = VALIDATION_SEL.get_or_init(|| {
243+
Selector::parse("input[name='__EVENTVALIDATION']").unwrap()
244+
});
245+
246+
let viewstate = document
247+
.select(viewstate_sel)
248+
.next()
249+
.and_then(|el| el.value().attr("value"))
250+
.unwrap_or("")
251+
.to_string();
252+
253+
let generator = document
254+
.select(generator_sel)
255+
.next()
256+
.and_then(|el| el.value().attr("value"))
257+
.unwrap_or("")
258+
.to_string();
259+
260+
let validation = document
261+
.select(validation_sel)
262+
.next()
263+
.and_then(|el| el.value().attr("value"))
264+
.unwrap_or("")
265+
.to_string();
266+
267+
let mut form_data: HashMap<&str, String> = HashMap::new();
268+
form_data.insert("__EVENTTARGET", "ctl00$plnMain$btnRefreshView".to_string());
269+
form_data.insert("__EVENTARGUMENT", "".to_string());
270+
form_data.insert("__LASTFOCUS", "".to_string());
271+
form_data.insert("__VIEWSTATE", viewstate);
272+
form_data.insert("__VIEWSTATEGENERATOR", generator);
273+
form_data.insert("__EVENTVALIDATION", validation);
274+
form_data.insert("ctl00$plnMain$ddlReportCardRuns", adjusted_six_weeks.to_string());
275+
form_data.insert("ctl00$plnMain$ddlClasses", "ALL".to_string());
276+
form_data.insert("ctl00$plnMain$ddlCompetencies", "ALL".to_string());
277+
form_data.insert("ctl00$plnMain$ddlOrderBy", "Class".to_string());
278+
279+
form_data
280+
}

0 commit comments

Comments
 (0)