Skip to content

Commit e3f73f2

Browse files
committed
Restructure routes, handlers foo
1 parent 79d7c3d commit e3f73f2

File tree

19 files changed

+577
-530
lines changed

19 files changed

+577
-530
lines changed

src/db.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,23 @@ impl Database {
346346
Ok(uid)
347347
}
348348

349+
/// Get title of a paste.
350+
pub async fn get_title(&self, id: Id) -> Result<String, Error> {
351+
let conn = self.conn.clone();
352+
let id = id.as_u32();
353+
354+
let title = spawn_blocking(move || {
355+
conn.lock().query_row(
356+
"SELECT title FROM entries WHERE id=?1",
357+
params![id],
358+
|row| Ok(row.get(0)?),
359+
)
360+
})
361+
.await??;
362+
363+
Ok(title)
364+
}
365+
349366
/// Delete `id`.
350367
pub async fn delete(&self, id: Id) -> Result<(), Error> {
351368
let conn = self.conn.clone();

src/handlers/delete.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use crate::handlers::html::{make_error, ErrorResponse};
2+
use crate::{Database, Error, Page};
3+
use axum::extract::{Path, State};
4+
use axum::response::Redirect;
5+
use axum_extra::extract::SignedCookieJar;
6+
7+
pub async fn delete(
8+
Path(id): Path<String>,
9+
State(db): State<Database>,
10+
State(page): State<Page>,
11+
jar: SignedCookieJar,
12+
) -> Result<Redirect, ErrorResponse> {
13+
async {
14+
let id = id.parse()?;
15+
let uid = db.get_uid(id).await?;
16+
let can_delete = jar
17+
.get("uid")
18+
.map(|cookie| cookie.value().parse::<i64>())
19+
.transpose()
20+
.map_err(|err| Error::CookieParsing(err.to_string()))?
21+
.zip(uid)
22+
.is_some_and(|(user_uid, db_uid)| user_uid == db_uid);
23+
24+
if !can_delete {
25+
Err(Error::Delete)?;
26+
}
27+
28+
db.delete(id).await?;
29+
30+
Ok(Redirect::to("/"))
31+
}
32+
.await
33+
.map_err(|err| make_error(err, page.clone()))
34+
}

src/handlers/download.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use crate::cache::Key;
2+
use crate::crypto::Password;
3+
use crate::handlers::html::{make_error, ErrorResponse, PasswordInput};
4+
use crate::{Database, Error, Page};
5+
use axum::extract::{Form, Path, State};
6+
use axum::http::header;
7+
use axum::response::{AppendHeaders, IntoResponse, Response};
8+
use axum_extra::headers::HeaderValue;
9+
use serde::Deserialize;
10+
11+
#[derive(Deserialize, Debug)]
12+
pub struct PasswordForm {
13+
password: String,
14+
}
15+
16+
/// GET handler for raw content of a paste.
17+
pub async fn download(
18+
Path(id): Path<String>,
19+
State(db): State<Database>,
20+
State(page): State<Page>,
21+
form: Option<Form<PasswordForm>>,
22+
) -> Result<Response, ErrorResponse> {
23+
async {
24+
let password = form.map(|form| Password::from(form.password.as_bytes().to_vec()));
25+
let key: Key = id.parse()?;
26+
27+
match db.get(key.id, password.clone()).await {
28+
Err(Error::NoPassword) => Ok(PasswordInput {
29+
page: page.clone(),
30+
id: key.id.to_string(),
31+
}
32+
.into_response()),
33+
Err(err) => Err(err),
34+
Ok(entry) => {
35+
if entry.must_be_deleted {
36+
db.delete(key.id).await?;
37+
}
38+
39+
Ok(get_download(entry.text, &key.id(), &key.ext).into_response())
40+
}
41+
}
42+
}
43+
.await
44+
.map_err(|err| make_error(err, page))
45+
}
46+
47+
fn get_download(text: String, id: &str, extension: &str) -> impl IntoResponse {
48+
let content_type = "text; charset=utf-8";
49+
let content_disposition =
50+
HeaderValue::from_str(&format!(r#"attachment; filename="{id}.{extension}"#))
51+
.expect("constructing valid header value");
52+
53+
(
54+
AppendHeaders([
55+
(header::CONTENT_TYPE, HeaderValue::from_static(content_type)),
56+
(header::CONTENT_DISPOSITION, content_disposition),
57+
]),
58+
text,
59+
)
60+
}

src/handlers/html/burn.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use crate::handlers::html::qr::{code_from, dark_modules};
2+
use crate::handlers::html::{make_error, ErrorResponse};
3+
use crate::{Error, Page};
4+
use askama::Template;
5+
use axum::extract::{Path, State};
6+
7+
/// GET handler for the burn page.
8+
pub async fn burn(Path(id): Path<String>, State(page): State<Page>) -> Result<Burn, ErrorResponse> {
9+
async {
10+
let code = tokio::task::spawn_blocking({
11+
let page = page.clone();
12+
let id = id.clone();
13+
move || code_from(&page.base_url, id)
14+
})
15+
.await
16+
.map_err(Error::from)??;
17+
18+
Ok(Burn {
19+
page: page.clone(),
20+
id,
21+
code,
22+
})
23+
}
24+
.await
25+
.map_err(|err| make_error(err, page))
26+
}
27+
28+
/// Burn page shown if "burn-after-reading" was selected during insertion.
29+
#[derive(Template)]
30+
#[template(path = "burn.html", escape = "none")]
31+
pub struct Burn {
32+
page: Page,
33+
id: String,
34+
code: qrcodegen::QrCode,
35+
}
36+
37+
impl Burn {
38+
fn dark_modules(&self) -> Vec<(i32, i32)> {
39+
dark_modules(&self.code)
40+
}
41+
}

src/handlers/html/index.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use crate::{AppState, Highlighter, Page};
2+
use askama::Template;
3+
use axum::extract::State;
4+
use std::num::{NonZero, NonZeroU32};
5+
use std::sync::OnceLock;
6+
7+
/// GET handler for the index page.
8+
pub async fn index(
9+
State(state): State<AppState>,
10+
State(page): State<Page>,
11+
State(highlighter): State<Highlighter>,
12+
) -> Index {
13+
Index {
14+
page,
15+
max_expiration: state.max_expiration,
16+
highlighter,
17+
}
18+
}
19+
20+
/// Index page displaying a form for paste insertion and a selection box for languages.
21+
#[derive(Template)]
22+
#[template(path = "index.html")]
23+
pub struct Index {
24+
page: Page,
25+
max_expiration: Option<NonZeroU32>,
26+
highlighter: Highlighter,
27+
}
28+
29+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
30+
enum Expiration {
31+
None,
32+
Burn,
33+
Time(NonZeroU32),
34+
}
35+
36+
impl std::fmt::Display for Expiration {
37+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+
match self {
39+
Expiration::None => write!(f, ""),
40+
Expiration::Burn => write!(f, "burn"),
41+
Expiration::Time(t) => write!(f, "{t}"),
42+
}
43+
}
44+
}
45+
46+
#[allow(clippy::unwrap_used)]
47+
const EXPIRATION_OPTIONS: [(&str, Expiration); 8] = [
48+
("never", Expiration::None),
49+
("10 minutes", Expiration::Time(NonZero::new(600).unwrap())),
50+
("1 hour", Expiration::Time(NonZero::new(3600).unwrap())),
51+
("1 day", Expiration::Time(NonZero::new(86400).unwrap())),
52+
("1 week", Expiration::Time(NonZero::new(604_800).unwrap())),
53+
(
54+
"1 month",
55+
Expiration::Time(NonZero::new(2_592_000).unwrap()),
56+
),
57+
(
58+
"1 year",
59+
Expiration::Time(NonZero::new(31_536_000).unwrap()),
60+
),
61+
("🔥 after reading", Expiration::Burn),
62+
];
63+
64+
impl Index {
65+
fn expiry_options(&self) -> &str {
66+
static EXPIRATION_OPTIONS_HTML: OnceLock<String> = OnceLock::new();
67+
68+
EXPIRATION_OPTIONS_HTML.get_or_init(|| {
69+
70+
let mut option_set = String::new();
71+
let mut wrote_first = false;
72+
73+
option_set.push('\n');
74+
75+
for (opt_name, opt_val) in EXPIRATION_OPTIONS {
76+
if self.max_expiration.is_none()
77+
|| opt_val == Expiration::Burn
78+
|| matches!((self.max_expiration, opt_val), (Some(exp), Expiration::Time(time)) if time <= exp)
79+
{
80+
option_set.push_str("<option");
81+
if !wrote_first {
82+
option_set.push_str(" selected");
83+
wrote_first = true;
84+
}
85+
option_set.push_str(" value=\"");
86+
option_set.push_str(opt_val.to_string().as_ref());
87+
option_set.push_str("\">");
88+
option_set.push_str(opt_name);
89+
option_set.push_str("</option>\n");
90+
}
91+
}
92+
93+
option_set
94+
})
95+
}
96+
}

src/handlers/html/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
pub mod burn;
2+
pub mod index;
3+
pub mod paste;
4+
pub mod qr;
5+
6+
pub use burn::burn;
7+
pub use index::index;
8+
pub use qr::qr;
9+
10+
use crate::{errors, Page};
11+
use askama::Template;
12+
use axum::http::StatusCode;
13+
14+
/// Error page showing a message.
15+
#[derive(Template)]
16+
#[template(path = "error.html")]
17+
pub struct Error {
18+
pub page: Page,
19+
pub description: String,
20+
}
21+
22+
/// Page showing password input.
23+
#[derive(Template)]
24+
#[template(path = "encrypted.html")]
25+
pub struct PasswordInput {
26+
pub page: Page,
27+
pub id: String,
28+
}
29+
30+
/// Error response carrying a status code and the page itself.
31+
pub type ErrorResponse = (StatusCode, Error);
32+
33+
/// Create an error response from `error` consisting of [`StatusCode`] derive from `error` as well
34+
/// as a rendered page with a description.
35+
pub fn make_error(error: errors::Error, page: Page) -> ErrorResponse {
36+
let description = error.to_string();
37+
(error.into(), Error { page, description })
38+
}

0 commit comments

Comments
 (0)