Use anyhow instead of error-chain for errors

This commit is contained in:
FliegendeWurst 2020-05-08 21:25:45 +02:00
parent fc9afdd66a
commit a3f84aa6fb
4 changed files with 57 additions and 108 deletions

57
Cargo.lock generated
View File

@ -4,7 +4,7 @@
name = "KIT-ILIAS-downloader" name = "KIT-ILIAS-downloader"
version = "0.2.4" version = "0.2.4"
dependencies = [ dependencies = [
"error-chain", "anyhow",
"futures-util", "futures-util",
"ignore", "ignore",
"lazy_static", "lazy_static",
@ -44,6 +44,12 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "anyhow"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "0.4.6" version = "0.4.6"
@ -86,28 +92,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "backtrace"
version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e"
dependencies = [
"backtrace-sys",
"cfg-if",
"libc",
"rustc-demangle",
]
[[package]]
name = "backtrace-sys"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.11.0" version = "0.11.0"
@ -332,7 +316,6 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd"
dependencies = [ dependencies = [
"backtrace",
"version_check", "version_check",
] ]
@ -956,18 +939,18 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "0.4.10" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36e3dcd42688c05a66f841d22c5d8390d9a5d4c9aaf57b9285eae4900a080063" checksum = "82c3bfbfb5bb42f99498c7234bbd768c220eb0cea6818259d0d18a1aa3d2595d"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "0.4.10" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4d7346ac577ff1296e06a418e7618e22655bae834d4970cb6e39d6da8119969" checksum = "ccbf6449dcfb18562c015526b085b8df1aa3cdab180af8ec2ebd300a3bd28f63"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -976,9 +959,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1277,12 +1260,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b386f4748bdae2aefc96857f5fda07647f851d089420e577831e2a14b45230f8" checksum = "b386f4748bdae2aefc96857f5fda07647f851d089420e577831e2a14b45230f8"
[[package]]
name = "rustc-demangle"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.17.0" version = "0.17.0"
@ -1367,9 +1344,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "0.4.3" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f331b9025654145cd425b9ded0caf8f5ae0df80d418b326e2dc1c3dc5eb0620" checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@ -1587,9 +1564,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.18" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" checksum = "e8e5aa70697bb26ee62214ae3288465ecec0000f05182f039b477001f08f5ae7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -9,7 +9,6 @@ edition = "2018"
[dependencies] [dependencies]
reqwest = { version = "0.10.4", default-features = false, features = ["cookies", "gzip", "json", "rustls-tls", "stream"] } reqwest = { version = "0.10.4", default-features = false, features = ["cookies", "gzip", "json", "rustls-tls", "stream"] }
error-chain = "0.12.2"
tokio = { version = "0.2", features = ["full"] } tokio = { version = "0.2", features = ["full"] }
serde_json = "1.0.51" serde_json = "1.0.51"
scraper = "0.11.0" scraper = "0.11.0"
@ -22,3 +21,4 @@ structopt = "0.3.13"
rpassword = "4.0.5" rpassword = "4.0.5"
rprompt = "1.0.5" rprompt = "1.0.5"
ignore = "0.4.14" ignore = "0.4.14"
anyhow = "1.0.28"

View File

@ -1,25 +0,0 @@
use error_chain::error_chain;
use super::*;
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
}
links {
}
foreign_links {
Io(std::io::Error);
Json(serde_json::Error);
Reqwest(reqwest::Error);
}
errors {
}
//skip_msg_variant
}

View File

@ -1,4 +1,4 @@
use error_chain::ChainedError; use anyhow::{Context, Result, anyhow};
use futures_util::stream::TryStreamExt; use futures_util::stream::TryStreamExt;
use ignore::gitignore::Gitignore; use ignore::gitignore::Gitignore;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -21,9 +21,6 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
mod errors;
use errors::*;
const ILIAS_URL: &'static str = "https://ilias.studium.kit.edu/"; const ILIAS_URL: &'static str = "https://ilias.studium.kit.edu/";
struct ILIAS { struct ILIAS {
@ -139,7 +136,7 @@ impl Object {
fn from_link(item: ElementRef, link: ElementRef) -> Result<Self> { fn from_link(item: ElementRef, link: ElementRef) -> Result<Self> {
let mut name = link.text().collect::<String>().replace('/', "-").trim().to_owned(); let mut name = link.text().collect::<String>().replace('/', "-").trim().to_owned();
let mut url = URL::from_href(link.value().attr("href").ok_or::<Error>("link missing href".into())?); let mut url = URL::from_href(link.value().attr("href").context("link missing href")?);
if url.thr_pk.is_some() { if url.thr_pk.is_some() {
return Ok(Thread { return Ok(Thread {
@ -194,7 +191,7 @@ impl Object {
}); });
} }
if url.target.as_ref().map(|x| x.starts_with("file_")).unwrap_or(false) { if url.target.as_ref().map(|x| x.starts_with("file_")).unwrap_or(false) {
let target = url.target.as_ref().ok_or("no download target")?; let target = url.target.as_ref().ok_or(anyhow!("no download target"))?;
if !target.ends_with("download") { if !target.ends_with("download") {
// download page containing metadata // download page containing metadata
return Ok(Generic { return Ok(Generic {
@ -204,8 +201,8 @@ impl Object {
} else { } else {
let item_prop = Selector::parse("span.il_ItemProperty").unwrap(); let item_prop = Selector::parse("span.il_ItemProperty").unwrap();
let mut item_props = item.select(&item_prop); let mut item_props = item.select(&item_prop);
let ext = item_props.next().ok_or("cannot find file extension")?; let ext = item_props.next().ok_or(anyhow!("cannot find file extension"))?;
let version = item_props.nth(1).ok_or("cannot find 3rd file metadata")?.text().collect::<String>(); let version = item_props.nth(1).ok_or(anyhow!("cannot find 3rd file metadata"))?.text().collect::<String>();
let version = version.trim(); let version = version.trim();
if version.starts_with("Version: ") { if version.starts_with("Version: ") {
name.push_str("_v"); name.push_str("_v");
@ -362,15 +359,15 @@ impl ILIAS {
login_soup = BeautifulSoup(otp_response.text, 'lxml') login_soup = BeautifulSoup(otp_response.text, 'lxml')
*/ */
let saml = Selector::parse(r#"input[name="SAMLResponse"]"#).unwrap(); let saml = Selector::parse(r#"input[name="SAMLResponse"]"#).unwrap();
let saml = dom.select(&saml).next().ok_or::<Error>("no SAML response, incorrect password?".into())?; let saml = dom.select(&saml).next().context("no SAML response, incorrect password?")?;
let relay_state = Selector::parse(r#"input[name="RelayState"]"#).unwrap(); let relay_state = Selector::parse(r#"input[name="RelayState"]"#).unwrap();
let relay_state = dom.select(&relay_state).next().ok_or::<Error>("no relay state".into())?; let relay_state = dom.select(&relay_state).next().context("no relay state")?;
println!("Logging into ILIAS.."); println!("Logging into ILIAS..");
this.client this.client
.post("https://ilias.studium.kit.edu/Shibboleth.sso/SAML2/POST") .post("https://ilias.studium.kit.edu/Shibboleth.sso/SAML2/POST")
.form(&json!({ .form(&json!({
"SAMLResponse": saml.value().attr("value").ok_or("no SAML value")?, "SAMLResponse": saml.value().attr("value").ok_or(anyhow!("no SAML value"))?,
"RelayState": relay_state.value().attr("value").ok_or("no RelayState value")? "RelayState": relay_state.value().attr("value").ok_or(anyhow!("no RelayState value"))?
})) }))
.send().await?; .send().await?;
println!("Logged in!"); println!("Logged in!");
@ -392,7 +389,7 @@ impl ILIAS {
let text = self.download(url).await?.text().await?; let text = self.download(url).await?.text().await?;
let html = Html::parse_document(&text); let html = Html::parse_document(&text);
if html.select(&alert_danger).next().is_some() { if html.select(&alert_danger).next().is_some() {
Err("ILIAS error".into()) Err(anyhow!("ILIAS error"))
} else { } else {
Ok(html) Ok(html)
} }
@ -402,7 +399,7 @@ impl ILIAS {
let text = self.download(url).await?.text().await?; let text = self.download(url).await?.text().await?;
let html = Html::parse_fragment(&text); let html = Html::parse_fragment(&text);
if html.select(&alert_danger).next().is_some() { if html.select(&alert_danger).next().is_some() {
Err("ILIAS error".into()) Err(anyhow!("ILIAS error"))
} else { } else {
Ok(html) Ok(html)
} }
@ -416,7 +413,7 @@ impl ILIAS {
.select(&container_item_title) .select(&container_item_title)
.next() .next()
.map(|link| Object::from_link(item, link)) .map(|link| Object::from_link(item, link))
.unwrap_or_else(|| Err("can't find link".into())) .unwrap_or_else(|| Err(anyhow!("can't find link")))
}).collect() }).collect()
} }
@ -471,18 +468,18 @@ async fn main() {
let ilias = match ILIAS::login(opt, user, pass).await { let ilias = match ILIAS::login(opt, user, pass).await {
Ok(ilias) => ilias, Ok(ilias) => ilias,
Err(e) => { Err(e) => {
print!("{}", e.display_chain()); print!("{:?}", e);
std::process::exit(77); std::process::exit(77);
} }
}; };
if ilias.opt.content_tree { if ilias.opt.content_tree {
// need this to get the content tree // need this to get the content tree
if let Err(e) = ilias.client.get("https://ilias.studium.kit.edu/ilias.php?baseClass=ilRepositoryGUI&cmd=frameset&set_mode=tree&ref_id=1").send().await { if let Err(e) = ilias.client.get("https://ilias.studium.kit.edu/ilias.php?baseClass=ilRepositoryGUI&cmd=frameset&set_mode=tree&ref_id=1").send().await {
println!("Warning: could not enable content tree: {}", e); println!("Warning: could not enable content tree: {:?}", e);
} }
} }
let ilias = Arc::new(ilias); let ilias = Arc::new(ilias);
let desktop = ilias.personal_desktop().await; let desktop = ilias.personal_desktop().await.context("Failed to load personal desktop");
match desktop { match desktop {
Ok(desktop) => { Ok(desktop) => {
for item in desktop.items { for item in desktop.items {
@ -494,7 +491,7 @@ async fn main() {
}); });
} }
}, },
Err(e) => println!("Error getting personal desktop: {}", e.display_chain()) Err(e) => println!("{:?}", e)
} }
// TODO: do this with tokio // TODO: do this with tokio
// https://github.com/tokio-rs/tokio/issues/2039 // https://github.com/tokio-rs/tokio/issues/2039
@ -504,7 +501,7 @@ async fn main() {
if ilias.opt.content_tree { if ilias.opt.content_tree {
// restore fast page loading times // restore fast page loading times
if let Err(e) = ilias.client.get("https://ilias.studium.kit.edu/ilias.php?baseClass=ilRepositoryGUI&cmd=frameset&set_mode=flat&ref_id=1").send().await { if let Err(e) = ilias.client.get("https://ilias.studium.kit.edu/ilias.php?baseClass=ilRepositoryGUI&cmd=frameset&set_mode=flat&ref_id=1").send().await {
println!("Warning: could not disable content tree: {}", e); println!("Warning: could not disable content tree: {:?}", e);
} }
} }
} }
@ -523,8 +520,8 @@ fn process_gracefully(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std
} }
*TASKS_RUNNING.lock() += 1; *TASKS_RUNNING.lock() += 1;
let path_text = format!("{:?}", path); let path_text = format!("{:?}", path);
if let Err(e) = process(ilias, path, obj).await { if let Err(e) = process(ilias, path, obj).await.context("Failed to process URL") {
print!("Error syncing {}: {}", path_text, e.display_chain()); print!("Syncing {:?}: {:?}", path_text, e);
} }
*TASKS_RUNNING.lock() -= 1; *TASKS_RUNNING.lock() -= 1;
*TASKS_QUEUED.lock() -= 1; *TASKS_QUEUED.lock() -= 1;
@ -580,7 +577,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
} }
let content = if ilias.opt.content_tree { let content = if ilias.opt.content_tree {
let html = ilias.download(&url.url).await?.text().await?; let html = ilias.download(&url.url).await?.text().await?;
let cmd_node = cmd_node_regex.find(&html).ok_or::<Error>("can't find cmdNode".into())?.as_str()[8..].to_owned(); let cmd_node = cmd_node_regex.find(&html).context("can't find cmdNode")?.as_str()[8..].to_owned();
let content_tree = ilias.get_course_content_tree(&url.ref_id, &cmd_node).await; let content_tree = ilias.get_course_content_tree(&url.ref_id, &cmd_node).await;
match content_tree { match content_tree {
Ok(tree) => tree, Ok(tree) => tree,
@ -590,7 +587,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
if html.contains(r#"input[name="cmd[join]""#) { if html.contains(r#"input[name="cmd[join]""#) {
return Ok(()); // ignore groups we are not in return Ok(()); // ignore groups we are not in
} }
println!("Warning: {:?} falling back to incomplete course content extractor! {}", name, e.display_chain()); println!("Warning: {:?} falling back to incomplete course content extractor! {:?}", name, e);
ilias.get_course_content(&url).await?.into_iter().flat_map(Result::ok).collect() // TODO: perhaps don't download almost the same content 3x ilias.get_course_content(&url).await?.into_iter().flat_map(Result::ok).collect() // TODO: perhaps don't download almost the same content 3x
} }
} }
@ -616,7 +613,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
for item in content { for item in content {
if item.is_err() { if item.is_err() {
if ilias.opt.verbose > 0 { if ilias.opt.verbose > 0 {
println!("Ignoring: {}", item.err().unwrap().display_chain()); println!("Ignoring: {:?}", item.err().unwrap());
} }
continue; continue;
} }
@ -681,7 +678,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
println!("Found video: {}", title); println!("Found video: {}", title);
} }
let video = Video { let video = Video {
url: URL::raw(link.value().attr("href").ok_or("video link without href")?.to_owned()) url: URL::raw(link.value().attr("href").ok_or(anyhow!("video link without href"))?.to_owned())
}; };
let ilias = Arc::clone(&ilias); let ilias = Arc::clone(&ilias);
task::spawn(async { task::spawn(async {
@ -712,11 +709,11 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
} }
let json: serde_json::Value = { let json: serde_json::Value = {
let mut json_capture = XOCT_REGEX.captures_iter(&html); let mut json_capture = XOCT_REGEX.captures_iter(&html);
let json = &json_capture.next().ok_or::<Error>("xoct player json not found".into())?[1]; let json = &json_capture.next().context("xoct player json not found")?[1];
if ilias.opt.verbose > 1 { if ilias.opt.verbose > 1 {
println!("{}", json); println!("{}", json);
} }
let json = json.split(",\n").nth(0).ok_or::<Error>("invalid xoct player json".into())?; let json = json.split(",\n").nth(0).context("invalid xoct player json")?;
serde_json::from_str(&json.trim())? serde_json::from_str(&json.trim())?
}; };
if ilias.opt.verbose > 1 { if ilias.opt.verbose > 1 {
@ -725,8 +722,8 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
let url = json let url = json
.pointer("/streams/0/sources/mp4/0/src") .pointer("/streams/0/sources/mp4/0/src")
.map(|x| x.as_str()) .map(|x| x.as_str())
.ok_or("video src not found")? .ok_or(anyhow!("video src not found"))?
.ok_or("video src not string")?; .ok_or(anyhow!("video src not string"))?;
let resp = ilias.download(&url).await?; let resp = ilias.download(&url).await?;
let mut reader = stream_reader(resp.bytes_stream().map_err(|x| { let mut reader = stream_reader(resp.bytes_stream().map_err(|x| {
io::Error::new(io::ErrorKind::Other, x) io::Error::new(io::ErrorKind::Other, x)
@ -757,7 +754,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
.flat_map(|x| x.value().attr("href")) .flat_map(|x| x.value().attr("href"))
.filter(|x| x.contains("trows=800")) .filter(|x| x.contains("trows=800"))
.next() .next()
.ok_or::<Error>("can't find forum thread count selector (empty forum?)".into())?.to_owned() .context("can't find forum thread count selector (empty forum?)")?.to_owned()
}; };
let data = ilias.download(&url); let data = ilias.download(&url);
let html = data.await?.text().await?; let html = data.await?.text().await?;
@ -769,11 +766,11 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
println!("Warning: unusual table row ({} cells) in {}", cells.len(), url); println!("Warning: unusual table row ({} cells) in {}", cells.len(), url);
continue; continue;
} }
let link = cells[1].select(&a).next().ok_or::<Error>("thread link not found".into())?; let link = cells[1].select(&a).next().context("thread link not found")?;
let object = Object::from_link(link, link)?; let object = Object::from_link(link, link)?;
let mut path = path.clone(); let mut path = path.clone();
let name = format!("{}_{}", let name = format!("{}_{}",
object.url().thr_pk.as_ref().ok_or::<Error>("thr_pk not found for thread".into())?, object.url().thr_pk.as_ref().context("thr_pk not found for thread")?,
link.text().collect::<String>().replace('/', "-").trim() link.text().collect::<String>().replace('/', "-").trim()
); );
path.push(name); path.push(name);
@ -784,7 +781,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
Err(_) => 0 Err(_) => 0
} }
}; };
let available_posts = cells[3].text().next().unwrap_or_default().trim().parse::<usize>().chain_err(|| "parsing post count failed")?; let available_posts = cells[3].text().next().unwrap_or_default().trim().parse::<usize>().context("parsing post count failed")?;
if available_posts <= saved_posts && !ilias.opt.force { if available_posts <= saved_posts && !ilias.opt.force {
continue; continue;
} }
@ -809,14 +806,14 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
} }
let html = ilias.get_html(&url.url).await?; let html = ilias.get_html(&url.url).await?;
for post in html.select(&post_row) { for post in html.select(&post_row) {
let title = post.select(&post_title).next().ok_or("post title not found")?.text().collect::<String>().replace('/', "-"); let title = post.select(&post_title).next().ok_or(anyhow!("post title not found"))?.text().collect::<String>().replace('/', "-");
let author = post.select(&span_small).next().ok_or("post author not found")?; let author = post.select(&span_small).next().ok_or(anyhow!("post author not found"))?;
let author = author.text().collect::<String>(); let author = author.text().collect::<String>();
let author = author.trim().split('|').nth(1).ok_or("author data in unknown format")?.trim(); let author = author.trim().split('|').nth(1).ok_or(anyhow!("author data in unknown format"))?.trim();
let container = post.select(&post_container).next().ok_or("post container not found")?; let container = post.select(&post_container).next().ok_or(anyhow!("post container not found"))?;
let link = container.select(&a).next().ok_or("post link not found")?; let link = container.select(&a).next().ok_or(anyhow!("post link not found"))?;
let name = format!("{}_{}_{}.html", link.value().attr("name").ok_or("post name in link not found")?, author, title.trim()); let name = format!("{}_{}_{}.html", link.value().attr("name").ok_or(anyhow!("post name in link not found"))?, author, title.trim());
let data = post.select(&post_content).next().ok_or("post content not found")?; let data = post.select(&post_content).next().ok_or(anyhow!("post content not found"))?;
let data = data.inner_html(); let data = data.inner_html();
let mut path = path.clone(); let mut path = path.clone();
path.push(name); path.push(name);
@ -851,7 +848,7 @@ fn process(ilias: Arc<ILIAS>, path: PathBuf, obj: Object) -> impl std::future::F
// not last page yet // not last page yet
let ilias = Arc::clone(&ilias); let ilias = Arc::clone(&ilias);
let next_page = Thread { let next_page = Thread {
url: URL::from_href(last.value().attr("href").ok_or("page link not found")?) url: URL::from_href(last.value().attr("href").ok_or(anyhow!("page link not found"))?)
}; };
task::spawn(async move { task::spawn(async move {
process_gracefully(ilias, path, next_page).await; process_gracefully(ilias, path, next_page).await;