Skip to main content

application_paste_bin/
application_paste_bin.rs

1//! A simple in memory pastebin backend
2// If you want to make a real paste bin you will need some sort of database for persistent storage
3
4// For a full pastebin front end and back end check out https://github.com/Basicprogrammer10/plaster-box
5// Or try it out at https://paste.connorcode.com
6
7use std::time::Instant;
8use std::{borrow::Borrow, sync::RwLock};
9
10use afire::internal::encoding::url;
11use afire::{Content, HeaderType, Method, Query, Response, Server, Status};
12
13const DATA_LIMIT: usize = 10_000;
14
15struct Paste {
16    name: String,
17    body: String,
18    time: Instant,
19}
20
21fn main() {
22    // Create Server
23    let mut server = Server::new("localhost", 8080).state(RwLock::new(Vec::new()));
24
25    // New paste interface
26    server.route(Method::GET, "/", |_req| {
27        Response::new().content(Content::HTML).text(
28            r#"
29        <form action="/new-form" method="post">
30        <input type="text" name="name" id="name" placeholder="Title">
31        
32        <br />
33        <textarea id="body" name="body" rows="5" cols="33"></textarea>
34        <input type="submit" value="Submit" />
35    </form>
36    "#,
37        )
38    });
39
40    // New paste API handler
41    server.stateful_route(Method::POST, "/new", move |app, req| {
42        // Make sure paste data isn't too long
43        if req.body.len() > DATA_LIMIT {
44            return Response::new()
45                .status(Status::NotFound)
46                .text("Data too big!");
47        }
48
49        // Get the data as string
50        let body_str = String::from_utf8_lossy(&req.body).to_string();
51
52        // Get the name from the Name header
53        let name = req.headers.get("Name").unwrap_or("Untitled");
54
55        let paste = Paste {
56            name: name.to_owned(),
57            body: body_str,
58            time: Instant::now(),
59        };
60
61        // Push this paste to the pastes vector
62        let mut pastes = app.write().unwrap();
63        let id = pastes.len();
64        pastes.push(paste);
65
66        // Send Redirect response
67        Response::new()
68            .status(Status::MovedPermanently)
69            .header(HeaderType::Location, format!("/p/{id}"))
70            .text(format!("Redirecting to /p/{id}."))
71    });
72
73    // New paste form handler
74    server.stateful_route(Method::POST, "/new-form", |app, req| {
75        // Get data from response
76        let query = Query::from_body(String::from_utf8_lossy(&req.body).borrow());
77        let name = url::decode(query.get("name").unwrap_or("Untitled")).expect("Invalid name");
78        let body = url::decode(query.get("body").expect("No body supplied")).expect("Invalid body");
79
80        // Make sure paste data isn't too long
81        if body.len() > DATA_LIMIT {
82            return Response::new()
83                .status(Status::NotFound)
84                .text("Data too big!");
85        }
86
87        let paste = Paste {
88            name,
89            body,
90            time: Instant::now(),
91        };
92
93        // Push this paste to the pastes vector
94        let mut pastes = app.write().unwrap();
95        let id = pastes.len();
96        pastes.push(paste);
97
98        // Send Redirect response
99        Response::new()
100            .status(Status::MovedPermanently)
101            .text("Ok")
102            .header(HeaderType::Location, format!("/p/{}", id))
103    });
104
105    // Get pate handler
106    server.stateful_route(Method::GET, "/p/{id}", move |app, req| {
107        // Get is from path param
108        let id = req.param("id").unwrap().parse::<usize>().unwrap();
109
110        // Get the paste by id
111        let paste = &app.read().unwrap()[id];
112
113        // Send paste
114        Response::new().text(&paste.body)
115    });
116
117    // View all pastes
118    server.stateful_route(Method::GET, "/pastes", move |app, _req| {
119        // Starter HTML
120        let mut out = String::from(
121            r#"<a href="/">New Paste</a><meta charset="UTF-8"><table><tr><th>Name</th><th>Date</th><th>Link</th></tr>"#,
122        );
123
124        // Add a table row for each paste
125        for (i, e) in app.read().unwrap().iter().enumerate() {
126            out.push_str(&format!(
127                "<tr><td>{}</td><td>{}</td><td><a href=\"/p/{}\">🔗</a></td></tr>",
128                e.name,
129                fmt_relative_time(e.time.elapsed().as_secs()),
130                i
131            ));
132        }
133
134        // Send HTML
135        Response::new()
136            .text(format!("{}</table>", out))
137            .content(Content::HTML)
138    });
139
140    server.start().unwrap();
141}
142
143const TIME_UNITS: &[(&str, u16)] = &[
144    ("second", 60),
145    ("minute", 60),
146    ("hour", 24),
147    ("day", 30),
148    ("month", 12),
149    ("year", 0),
150];
151
152/// Turn relative number of seconds into a more readable relative time.
153/// If the time is 0, now will be returned.
154/// Ex 1 minute ago or 3 years ago
155pub fn fmt_relative_time(secs: u64) -> String {
156    if secs == 0 {
157        return "now".into();
158    }
159
160    let mut secs = secs as f64;
161    for i in TIME_UNITS {
162        if i.1 == 0 || secs < i.1 as f64 {
163            secs = secs.round();
164            return format!("{} {}{} ago", secs, i.0, if secs > 1.0 { "s" } else { "" });
165        }
166
167        secs /= i.1 as f64;
168    }
169
170    format!("{} years ago", secs.round())
171}
172
173// To use POST to /new with the body set to your paste data
174// You can then GET /pastes to see all the pastes