diff --git a/Cargo.toml b/Cargo.toml
index ad68320..94b99e8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,3 +11,5 @@ askama = "0.10"
askama-filters = { version = "0.1.3", features = ["chrono"] }
chrono = "0.4.19"
rand = "0.8.5"
+linkify = "0.8.1"
+clap = { version = "3.1.12", features = ["derive"] }
diff --git a/README.MD b/README.MD
index db42eb2..457df0b 100644
--- a/README.MD
+++ b/README.MD
@@ -2,10 +2,10 @@
![Screenshot](git/index.png)
-MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is around 6MB and it uses 2MB memory (plus your pastas).
+MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is only a few megabytes and uses very little memory (plus your pastas).
### Installation
-Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start on port 8080.
+Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start on port 8080 but you can change this with `-p` or `--port` arguments.
To install it as a service on your Linux machine, create a file called `/etc/systemd/system/microbin.service`, paste this into it with the value of `ExecStart` replaced with the actual path to microbin on your machine.
@@ -24,15 +24,16 @@ WantedBy=multi-user.target
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`.
### Features
-- No CSS or JS, super lightweight and simple
+- Very little CSS and no JS, super lightweight and simple (see [water.css](https://github.com/kognise/water.css))
- Animal names instead of random numbers for pasta identifiers
- Automatically expiring pastas
- Never expiring pastas
- Listing and manually removing pastas
+- URL shortening and redirection
### Needed improvements
- Persisting pastas on disk (currently they are lost on restart)
- Removing pasta after N reads
- File uploads
-- URL shortening
+- ~~URL shortening~~ (added on 23 April 2022)
diff --git a/git/index.png b/git/index.png
index 27ea961..3b16e64 100644
Binary files a/git/index.png and b/git/index.png differ
diff --git a/src/main.rs b/src/main.rs
index 1d08c81..d76ce71 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,23 +1,32 @@
extern crate core;
+use actix_files as fs;
+use actix_web::web::Data;
+use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder, Result};
+use askama::Template;
+use clap::Parser;
+use linkify::{LinkFinder, LinkKind};
+use rand::Rng;
+use regex::Regex;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
-use actix_files::NamedFile;
-use actix_web::{App, get, HttpRequest, HttpResponse, HttpServer, post, Responder, Result, web};
-use actix_web::web::Data;
-use askama::Template;
-use rand::Rng;
-
use crate::animalnumbers::{to_animal_names, to_u64};
use crate::pasta::{Pasta, PastaFormData};
-mod pasta;
mod animalnumbers;
+mod pasta;
struct AppState {
- pastas: Mutex>,
+ pastas: Mutex>,
+}
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+struct Args {
+ #[clap(short, long, default_value_t = 8080)]
+ port: u32,
}
#[derive(Template)]
@@ -27,130 +36,192 @@ struct IndexTemplate {}
#[derive(Template)]
#[template(path = "pasta.html")]
struct PastaTemplate<'a> {
- pasta: &'a Pasta,
+ pasta: &'a Pasta,
}
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
- pastas: &'a Vec,
+ pastas: &'a Vec,
}
#[get("/")]
async fn index() -> impl Responder {
- HttpResponse::Found().content_type("text/html").body(IndexTemplate {}.render().unwrap())
+ HttpResponse::Found()
+ .content_type("text/html")
+ .body(IndexTemplate {}.render().unwrap())
}
#[post("/create")]
async fn create(data: web::Data, pasta: web::Form) -> impl Responder {
- let mut pastas = data.pastas.lock().unwrap();
+ let mut pastas = data.pastas.lock().unwrap();
- let mut innerPasta = pasta.into_inner();
+ let inner_pasta = pasta.into_inner();
- let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
- Ok(n) => n.as_secs(),
- Err(_) => panic!("SystemTime before UNIX EPOCH!"),
- } as i64;
+ let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
+ Ok(n) => n.as_secs(),
+ Err(_) => panic!("SystemTime before UNIX EPOCH!"),
+ } as i64;
- let expiration = match innerPasta.expiration.as_str() {
- "1min" => timenow + 60,
- "10min" => timenow + 60 * 10,
- "1hour" => timenow + 60 * 60,
- "24hour" => timenow + 60 * 60 * 24,
- "1week" => timenow + 60 * 60 * 24 * 7,
- "never" => 0,
- _ => panic!("Unexpected expiration time!")
- };
+ let expiration = match inner_pasta.expiration.as_str() {
+ "1min" => timenow + 60,
+ "10min" => timenow + 60 * 10,
+ "1hour" => timenow + 60 * 60,
+ "24hour" => timenow + 60 * 60 * 24,
+ "1week" => timenow + 60 * 60 * 24 * 7,
+ "never" => 0,
+ _ => panic!("Unexpected expiration time!"),
+ };
- let mut newPasta = Pasta {
- id: rand::thread_rng().gen::() as u64,
- content: innerPasta.content,
- created: timenow,
- expiration,
- };
+ let pasta_type = if is_valid_url(inner_pasta.content.as_str()) {
+ String::from("url")
+ } else {
+ String::from("text")
+ };
- let id = newPasta.id;
+ let new_pasta = Pasta {
+ id: rand::thread_rng().gen::() as u64,
+ content: inner_pasta.content,
+ created: timenow,
+ pasta_type,
+ expiration,
+ };
- pastas.push(newPasta);
+ let id = new_pasta.id;
- HttpResponse::Found().append_header(("Location", format!("/pasta/{}", to_animal_names(id)))).finish()
+ pastas.push(new_pasta);
+
+ HttpResponse::Found()
+ .append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
+ .finish()
}
#[get("/pasta/{id}")]
async fn getpasta(data: web::Data, id: web::Path) -> HttpResponse {
- let mut pastas = data.pastas.lock().unwrap();
- let id = to_u64(&*id.into_inner());
+ let mut pastas = data.pastas.lock().unwrap();
+ let id = to_u64(&*id.into_inner());
- remove_expired(&mut pastas);
+ remove_expired(&mut pastas);
- for pasta in pastas.iter() {
- if pasta.id == id {
- return HttpResponse::Found().content_type("text/html").body(PastaTemplate { pasta }.render().unwrap());
- }
- }
+ for pasta in pastas.iter() {
+ if pasta.id == id {
+ return HttpResponse::Found()
+ .content_type("text/html")
+ .body(PastaTemplate { pasta }.render().unwrap());
+ }
+ }
- HttpResponse::Found().body("Pasta not found! :-(")
+ HttpResponse::Found().body("Pasta not found! :-(")
}
-#[get("/rawpasta/{id}")]
+#[get("/url/{id}")]
+async fn redirecturl(data: web::Data, id: web::Path) -> HttpResponse {
+ let mut pastas = data.pastas.lock().unwrap();
+ let id = to_u64(&*id.into_inner());
+
+ remove_expired(&mut pastas);
+
+ for pasta in pastas.iter() {
+ if pasta.id == id {
+ if pasta.pasta_type == "url" {
+ return HttpResponse::Found()
+ .append_header(("Location", String::from(&pasta.content)))
+ .finish();
+ } else {
+ return HttpResponse::Found().body("This is not a valid URL. :-(");
+ }
+ }
+ }
+
+ HttpResponse::Found().body("Pasta not found! :-(")
+}
+
+#[get("/raw/{id}")]
async fn getrawpasta(data: web::Data, id: web::Path) -> String {
- let mut pastas = data.pastas.lock().unwrap();
- let id = to_u64(&*id.into_inner());
+ let mut pastas = data.pastas.lock().unwrap();
+ let id = to_u64(&*id.into_inner());
- remove_expired(&mut pastas);
+ remove_expired(&mut pastas);
- for pasta in pastas.iter() {
- if pasta.id == id {
- return pasta.content.to_owned();
- }
- }
+ for pasta in pastas.iter() {
+ if pasta.id == id {
+ return pasta.content.to_owned();
+ }
+ }
- String::from("Pasta not found! :-(")
+ String::from("Pasta not found! :-(")
}
#[get("/remove/{id}")]
async fn remove(data: web::Data, id: web::Path) -> HttpResponse {
- let mut pastas = data.pastas.lock().unwrap();
- let id = to_u64(&*id.into_inner());
+ let mut pastas = data.pastas.lock().unwrap();
+ let id = to_u64(&*id.into_inner());
- remove_expired(&mut pastas);
+ remove_expired(&mut pastas);
- for (i, pasta) in pastas.iter().enumerate() {
- if pasta.id == id {
- pastas.remove(i);
- return HttpResponse::Found().append_header(("Location", "/pastalist")).finish();
- }
- }
+ for (i, pasta) in pastas.iter().enumerate() {
+ if pasta.id == id {
+ pastas.remove(i);
+ return HttpResponse::Found()
+ .append_header(("Location", "/pastalist"))
+ .finish();
+ }
+ }
- HttpResponse::Found().body("Pasta not found! :-(")
+ HttpResponse::Found().body("Pasta not found! :-(")
}
#[get("/pastalist")]
async fn list(data: web::Data) -> HttpResponse {
- let mut pastas = data.pastas.lock().unwrap();
+ let mut pastas = data.pastas.lock().unwrap();
- remove_expired(&mut pastas);
+ remove_expired(&mut pastas);
- HttpResponse::Found().content_type("text/html").body(PastaListTemplate { pastas: &pastas }.render().unwrap())
+ HttpResponse::Found()
+ .content_type("text/html")
+ .body(PastaListTemplate { pastas: &pastas }.render().unwrap())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
- let data = web::Data::new(AppState {
- pastas: Mutex::new(Vec::new()),
- });
+ let args = Args::parse();
+ println!(
+ "{}",
+ format!("Listening on http://127.0.0.1:{}", args.port.to_string())
+ );
- HttpServer::new(move || App::new().app_data(data.clone()).service(index).service(create).service(getpasta).service(getrawpasta).service(remove).service(list)
- ).bind("127.0.0.1:8080")?.run().await
+ let data = web::Data::new(AppState {
+ pastas: Mutex::new(Vec::new()),
+ });
+
+ HttpServer::new(move || {
+ App::new()
+ .app_data(data.clone())
+ .service(index)
+ .service(create)
+ .service(getpasta)
+ .service(redirecturl)
+ .service(getrawpasta)
+ .service(remove)
+ .service(list)
+ .service(fs::Files::new("/static", "./static"))
+ })
+ .bind(format!("127.0.0.1:{}", args.port.to_string()))?
+ .run()
+ .await
}
fn remove_expired(pastas: &mut Vec) {
- let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
- Ok(n) => n.as_secs(),
- Err(_) => panic!("SystemTime before UNIX EPOCH!"),
- } as i64;
+ let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
+ Ok(n) => n.as_secs(),
+ Err(_) => panic!("SystemTime before UNIX EPOCH!"),
+ } as i64;
- pastas.retain(|p| {
- p.expiration == 0 || p.expiration > timenow
- });
+ pastas.retain(|p| p.expiration == 0 || p.expiration > timenow);
+}
+
+fn is_valid_url(url: &str) -> bool {
+ let finder = LinkFinder::new();
+ let spans: Vec<_> = finder.spans(url).collect();
+ spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}
diff --git a/src/pasta.rs b/src/pasta.rs
index 9df8921..dbeaff3 100644
--- a/src/pasta.rs
+++ b/src/pasta.rs
@@ -8,7 +8,8 @@ pub struct Pasta {
pub id: u64,
pub content: String,
pub created: i64,
- pub expiration: i64
+ pub expiration: i64,
+ pub pasta_type: String
}
#[derive(Deserialize)]
diff --git a/templates/header.html b/templates/header.html
index 1434ed7..2bfb0ad 100644
--- a/templates/header.html
+++ b/templates/header.html
@@ -3,22 +3,26 @@
MicroBin
-
+
+
+ font-size: 1.1em;
+ ">
- MicroBin
+ μ MicroBin
-|
-New Pasta
-|
-Pasta List
-|
-GitHub
-
\ No newline at end of file
+New Pasta
+
+Pasta List
+
+GitHub
+
+
diff --git a/templates/index.html b/templates/index.html
index b75866b..95f34b4 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,6 +1,6 @@
{% include "header.html" %}
-{% include "footer.html" %}
\ No newline at end of file
+{% include "footer.html" %}
diff --git a/templates/pasta.html b/templates/pasta.html
index 4f58c08..1b7a8c5 100644
--- a/templates/pasta.html
+++ b/templates/pasta.html
@@ -1,6 +1,4 @@
{% include "header.html" %}
-Raw Pasta
-
-{{pasta}}
-
-{% include "footer.html" %}
\ No newline at end of file
+Raw Pasta
+{{pasta}}
+{% include "footer.html" %}
diff --git a/templates/pastalist.html b/templates/pastalist.html
index 741f1e7..4369153 100644
--- a/templates/pastalist.html
+++ b/templates/pastalist.html
@@ -6,8 +6,13 @@
No pastas yet. 😔 Create one here.
{%- else %}
-
-
+
+
+
+
+ Pastas |
+
+
Key
|
@@ -21,22 +26,71 @@
+
+
{% for pasta in pastas %}
-
-
- {{pasta.idAsAnimals()}}
- |
-
- {{pasta.createdAsString()}}
- |
-
- {{pasta.expirationAsString()}}
- |
-
- Remove
- |
-
+ {% if pasta.pasta_type == "text" %}
+
+
+ {{pasta.idAsAnimals()}}
+ |
+
+ {{pasta.createdAsString()}}
+ |
+
+ {{pasta.expirationAsString()}}
+ |
+
+ Raw
+ Remove
+ |
+
+ {%- endif %}
{% endfor %}
+
+
+
+
+
+
+ URL Redirects |
+
+
+
+ Key
+ |
+
+ Created
+ |
+
+ Expiration
+ |
+
+
+ |
+
+
+ {% for pasta in pastas %}
+ {% if pasta.pasta_type == "url" %}
+
+
+ {{pasta.idAsAnimals()}}
+ |
+
+ {{pasta.createdAsString()}}
+ |
+
+ {{pasta.expirationAsString()}}
+ |
+
+ Raw
+ Remove
+ |
+
+ {%- endif %}
+ {% endfor %}
+
+
{%- endif %}
-{% include "footer.html" %}
\ No newline at end of file
+{% include "footer.html" %}