diff --git a/.gitignore b/.gitignore index ea8c4bf..f30d81c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +data.pdf diff --git a/Cargo.lock b/Cargo.lock index efd1297..61e119d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,214 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util 0.7.1", +] + +[[package]] +name = "actix-files" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81bde9a79336aa51ebed236e91fc1a0528ff67cfdf4f68ca4c61ede9fd26fb5" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + +[[package]] +name = "actix-http" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa 1.0.1", + "language-tags", + "local-channel", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha-1", + "smallvec", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb60846b52c118f2f04a56cc90880a274271c489b2498623d58176f8ca21fa80" +dependencies = [ + "bytestring", + "firestorm", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e5ebffd51d50df56a3ae0de0e59487340ca456f05dd0b90c0a7a6dd6a74d31" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa 1.0.1", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.9", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7525bedf54704abb1d469e88d7e7e9226df73778798a69cea5022d53b2ae91bc" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler32" version = "1.2.0" @@ -39,6 +247,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.6", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -48,6 +267,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + [[package]] name = "autocfg" version = "1.1.0" @@ -75,6 +315,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "block-modes" version = "0.7.0" @@ -91,6 +340,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.9.1" @@ -109,11 +379,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bytestring" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +dependencies = [ + "bytes", +] + [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -149,6 +431,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "percent-encoding", + "time 0.3.9", + "version_check", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -158,6 +451,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -216,6 +528,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -278,6 +600,24 @@ dependencies = [ "syn", ] +[[package]] +name = "firestorm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d6188b8804df28032815ea256b6955c9625c24da7525f387a7af02fbb8f01" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -519,6 +859,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.6.0" @@ -625,6 +971,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -640,6 +995,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -652,6 +1013,24 @@ version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +[[package]] +name = "local-channel" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6246c68cf195087205a0512559c97e15eaf95198bf0e206d662092cdcb03fe9f" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902eb695eb0591864543cbfbf6d742510642a605a61fc5e97fe6ceb5a30ac4fb" + [[package]] name = "lock_api" version = "0.4.7" @@ -715,6 +1094,26 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.8.2" @@ -838,6 +1237,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + [[package]] name = "pdf" version = "0.7.2" @@ -972,6 +1377,8 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" name = "poop-graph" version = "0.1.0" dependencies = [ + "actix-files", + "actix-web", "bytes", "futures", "lazy_static", @@ -982,6 +1389,7 @@ dependencies = [ "thiserror", "time 0.3.9", "tokio", + "tokio-stream", "tokio-util 0.7.1", "url", ] @@ -1316,16 +1724,27 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -1569,6 +1988,17 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.9" @@ -1610,6 +2040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1647,6 +2078,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -1906,3 +2346,32 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] + +[[package]] +name = "zstd" +version = "0.10.0+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml index a22fb9d..bddf24e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ authors = ["xenofem "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +actix-files = "0.6.0" +actix-web = "4.0.1" bytes = "1.1" futures = "0.3" lazy_static = "1.4" @@ -17,5 +19,6 @@ scraper = "0.12" thiserror = "1" time = { version = "0.3.9", features = ["formatting", "macros", "parsing"] } tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1.8" tokio-util = { version = "0.7", features = ["codec"] } url = "2.2.2" diff --git a/src/extract.rs b/src/extract.rs index c91fda3..1ca18ec 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,6 +1,4 @@ -use std::collections::HashMap; -use std::fmt::Write; -use std::rc::Rc; +use std::{collections::HashMap, fmt::Write, sync::Arc}; use lazy_static::lazy_static; use pdf::{backend::Backend, content::Operation, primitive::Primitive}; @@ -21,15 +19,21 @@ const DATE_DISPLAY_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!("[year]-[month]-[day]"); pub struct DataSet { - pub columns: Vec>, + pub columns: Vec>, pub rows: Vec, } +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("PDF contained no data rows")] + NoData, +} + impl DataSet { - pub fn extract(doc: &pdf::file::File) -> Option { + pub fn extract(doc: &pdf::file::File) -> Result { let mut doc_iter = DocumentIterator::new(doc).peekable(); - let mut columns: Vec<(Rc, f32)> = Vec::new(); + let mut columns: Vec<(Arc, f32)> = Vec::new(); let mut rows: Vec = Vec::new(); let (mut current_datapoint, mut current_y) = loop { @@ -39,7 +43,7 @@ impl DataSet { let column_x = text.x; while let Some(more) = doc_iter.peek() { if is_new_column_header(&more.text) || DATE_REGEX.is_match(&more.text) { - columns.push((Rc::new(column_name), column_x)); + columns.push((Arc::new(column_name), column_x)); break; } column_name += " "; @@ -53,7 +57,7 @@ impl DataSet { ); } } else { - return None; + return Err(Error::NoData); } }; @@ -79,40 +83,44 @@ impl DataSet { } } - Some(Self { + Ok(Self { columns: columns.into_iter().map(|(column, _)| column).collect(), rows, }) } + pub fn csv_header(&self) -> Result { + let mut header = String::from("Date"); + for column in self.columns.iter() { + write!(&mut header, ",{}", column)?; + } + Ok(header) + } + + pub fn csv_row(&self, datapoint: &DataPoint) -> Result { + let mut csv_row = datapoint + .date + .format(DATE_DISPLAY_FORMAT) + .expect("Failed to format date!"); + for column in self.columns.iter() { + if let Some(val) = datapoint.values.get(column) { + write!(&mut csv_row, ",{}", val)?; + } else { + write!(&mut csv_row, ",")?; + } + } + Ok(csv_row) + } + pub fn csv_rows(&self) -> impl Iterator> + '_ { - std::iter::once_with(|| { - let mut header = String::from("Date"); - for column in self.columns.iter() { - write!(&mut header, ",{}", column)?; - } - Ok(header) - }) - .chain(self.rows.iter().map(|datapoint| { - let mut csv_row = datapoint - .date - .format(DATE_DISPLAY_FORMAT) - .expect("Failed to format date!"); - for column in self.columns.iter() { - if let Some(val) = datapoint.values.get(column) { - write!(&mut csv_row, ",{}", val)?; - } else { - write!(&mut csv_row, ",")?; - } - } - Ok(csv_row) - })) + std::iter::once_with(|| self.csv_header()) + .chain(self.rows.iter().map(|datapoint| self.csv_row(datapoint))) } } pub struct DataPoint { pub date: Date, - pub values: HashMap, u32>, + pub values: HashMap, u32>, } impl DataPoint { diff --git a/src/main.rs b/src/main.rs index fd57abe..cb48873 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,118 @@ +use std::{sync::Arc, time::Duration}; + +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use lazy_static::lazy_static; +use tokio::sync::RwLock; + mod extract; mod fetch; use extract::DataSet; use fetch::PdfFetcher; -#[tokio::main] -async fn main() { - let mut fetcher = PdfFetcher::new().expect("Failed to initialize PDF fetcher"); - let doc = fetcher.fetch().await.expect("Failed to fetch PDF"); - let dataset = DataSet::extract(&doc).expect("Failed to extract dataset"); - for row in dataset.csv_rows() { - println!("{}", row.unwrap()); +lazy_static! { + static ref UPDATE_INTERVAL: Duration = Duration::from_secs(3600); +} + +struct AppState { + dataset: RwLock>, +} + +#[derive(thiserror::Error, Debug)] +enum Error { + #[error("Failed to fetch PDF")] + Fetch(#[from] fetch::Error), + #[error("Failed to extract data from PDF")] + Extract(#[from] extract::Error), +} + +async fn load_data(fetcher: &mut PdfFetcher) -> Result { + Ok(DataSet::extract(&fetcher.fetch().await?)?) +} + +async fn try_update(state: &AppState, fetcher: &mut PdfFetcher) -> Result<(), Error> { + *state.dataset.write().await = Arc::new(load_data(fetcher).await?); + Ok(()) +} + +async fn start_updater() -> Result, Error> { + let mut fetcher = PdfFetcher::new()?; + let state = web::Data::new(AppState { + dataset: RwLock::new(Arc::new(load_data(&mut fetcher).await?)), + }); + + let state_copy = state.clone(); + std::thread::spawn(move || { + actix_web::rt::System::new().block_on(async { + loop { + if let Err(e) = try_update(&state_copy, &mut fetcher).await { + eprintln!("Error updating data: {:#?}", e); + } + actix_web::rt::time::sleep(*UPDATE_INTERVAL).await; + } + }); + }); + + Ok(state) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let state = start_updater().await.expect("Failed to initialize state"); + + HttpServer::new(move || { + App::new() + .app_data(state.clone()) + .service(csv) + .service(actix_files::Files::new("/", "./static/").index_file("index.html")) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} + +struct DataIterator { + dataset: Arc, + index: Option, +} + +impl DataIterator { + fn new(dataset: Arc) -> Self { + Self { + dataset, + index: None, + } } } + +impl Iterator for DataIterator { + type Item = Result; + + fn next(&mut self) -> Option { + match self.index { + None => { + self.index = Some(0); + Some(self.dataset.csv_header().map(|s| s + "\n")) + } + Some(i) => { + if let Some(row) = self.dataset.rows.get(i) { + self.index = Some(i + 1); + Some(self.dataset.csv_row(row).map(|s| s + "\n")) + } else { + None + } + } + } + } +} + +#[get("/data.csv")] +async fn csv(data: web::Data) -> impl Responder { + let dataset = { data.dataset.read().await.clone() }; + + let rows = + tokio_stream::iter(DataIterator::new(dataset).map(|item| item.map(bytes::Bytes::from))); + HttpResponse::Ok() + .content_type("text/csv; charset=utf-8") + .body(actix_web::body::BodyStream::new(rows)) +}