application_quote_book/
application_quote_book.rs1use std::{
7 collections::HashMap,
8 fs,
9 net::Ipv4Addr,
10 path::PathBuf,
11 sync::RwLock,
12 time::{SystemTime, UNIX_EPOCH},
13};
14
15use afire::{
16 extension::date::imp_date,
17 internal::encoding::url,
18 trace,
19 trace::{set_log_level, Level},
20 Content, HeaderType, Method, Query, Response, Server, Status,
21};
22
23struct App {
24 path: PathBuf,
25 quotes: RwLock<HashMap<String, Quote>>,
26}
27
28struct Quote {
29 name: String,
30 value: String,
31 date: u64,
32}
33
34fn main() {
35 set_log_level(Level::Trace);
36 let app = App::new(PathBuf::from("quotes.txt"));
37 app.load();
38
39 let mut server = Server::new(Ipv4Addr::LOCALHOST, 8080).state(app);
40
41 server.route(Method::GET, "/", |_| {
43 Response::new()
44 .text(String::new() + HEADER + HOME)
45 .content(Content::HTML)
46 });
47
48 server.stateful_route(Method::POST, "/api/new", |app, req| {
51 let form = Query::from_body(&String::from_utf8_lossy(&req.body));
52 let name =
53 url::decode(form.get("author").expect("No author supplied")).expect("Invalid author");
54 let body =
55 url::decode(form.get("quote").expect("No quote supplied")).expect("Invalid quote");
56
57 let quote = Quote {
58 name,
59 value: body,
60 date: now(),
61 };
62 let mut quotes = app.quotes.write().unwrap();
63 let id = quotes.len();
64 quotes.insert(id.to_string(), quote);
65 drop(quotes);
66 trace!(Level::Trace, "Added new quote #{id}");
67
68 app.save();
69 Response::new()
70 .status(Status::SeeOther)
71 .header(HeaderType::Location, format!("/quote/{id}"))
72 .text("Redirecting to quote page.")
73 });
74
75 server.stateful_route(Method::GET, "/quote/{id}", |app, req| {
76 let id = req.param("id").unwrap();
77 if id == "undefined" {
78 return Response::new();
79 }
80
81 let id = id.parse::<usize>().expect("ID is not a valid integer");
82 let quotes = app.quotes.read().unwrap();
83 if id >= quotes.len() {
84 return Response::new()
85 .status(Status::NotFound)
86 .text(format!("No quote with the id {id} was found."));
87 }
88
89 let quote = quotes.get(&id.to_string()).unwrap();
90 Response::new().content(Content::HTML).text(
91 String::new()
92 + HEADER
93 + "E
94 .replace("{QUOTE}", "e.value)
95 .replace("{AUTHOR}", "e.name)
96 .replace("{TIME}", &imp_date(quote.date)),
97 )
98 });
99
100 server.stateful_route(Method::GET, "/quotes", |app, _req| {
101 let mut out = String::from(HEADER);
102 out.push_str("<ul>");
103 for i in app.quotes.read().unwrap().iter() {
104 out.push_str(&format!(
105 "<li><a href=\"/quote/{}\">\"{}\" - {}</a></li>\n",
106 i.0, i.1.name, i.1.value
107 ));
108 }
109
110 Response::new().text(out + "</ul>").content(Content::HTML)
111 });
112
113 server.start().unwrap();
115}
116
117fn now() -> u64 {
118 SystemTime::now()
119 .duration_since(UNIX_EPOCH)
120 .expect("Time went backwards")
121 .as_secs()
122}
123
124impl App {
125 fn new(path: PathBuf) -> Self {
126 Self {
127 path,
128 quotes: RwLock::new(HashMap::new()),
129 }
130 }
131
132 fn load(&self) {
133 if !self.path.exists() {
134 trace!(Level::Trace, "No save file found. Skipping loading.");
135 return;
136 }
137
138 let data = fs::read_to_string(&self.path).unwrap();
139 let mut quotes = self.quotes.write().unwrap();
140 quotes.clear();
141
142 for i in data.lines() {
143 let (name, quote) = i.split_once(':').unwrap();
144 if let Some(i) = Quote::load(quote) {
145 quotes.insert(name.to_owned(), i);
146 continue;
147 }
148 trace!(Level::Error, "Error loading entry");
149 }
150
151 trace!("Loaded {} entries", quotes.len());
152 }
153
154 fn save(&self) {
155 trace!(Level::Trace, "Saving quotes");
156 let mut out = String::new();
157
158 for i in self.quotes.read().unwrap().iter() {
159 out.push_str(&format!("{}:{}\n", i.0, i.1.save()));
160 }
161
162 fs::write(&self.path, out).unwrap();
163 }
164}
165
166impl Quote {
167 fn save(&self) -> String {
168 format!(
169 "{}:{}:{}",
170 url::encode(&self.name),
171 url::encode(&self.value),
172 self.date
173 )
174 }
175
176 fn load(line: &str) -> Option<Self> {
177 let mut parts = line.split(':');
178 let name = url::decode(parts.next()?).unwrap();
179 let value = url::decode(parts.next()?).unwrap();
180 let date = parts.next()?.parse().ok()?;
181
182 Some(Self { name, value, date })
183 }
184}
185
186const HEADER: &str = r#"
192<a href="/">New Quote</a> •
193<a href="/quotes">All Quotes</a>
194"#;
195
196const HOME: &str = r#"
198<form method="post" action="/api/new">
199 <label for="author">Author:</label>
200 <input type="text" name="author" required>
201 <br>
202 <label for="quote">Quote:</label>
203 <textarea name="quote" id="quote" cols="30" rows="4"></textarea>
204 <br>
205 <input type="submit" value="Submit">
206</form>
207"#;
208
209const QUOTE: &str = r#"
210<p>"{QUOTE}"</p>
211<p> - {AUTHOR} ({TIME})</p>
212"#;