- added new pasta type URL and automatic redirection endpoint
- added water.css styling - added CL argument to set port - added raw URL to pasta list
This commit is contained in:
parent
e8b0e3a482
commit
f56ffa98e4
9 changed files with 251 additions and 120 deletions
|
@ -11,3 +11,5 @@ askama = "0.10"
|
||||||
askama-filters = { version = "0.1.3", features = ["chrono"] }
|
askama-filters = { version = "0.1.3", features = ["chrono"] }
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
linkify = "0.8.1"
|
||||||
|
clap = { version = "3.1.12", features = ["derive"] }
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
![Screenshot](git/index.png)
|
![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
|
### 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.
|
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`.
|
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`.
|
||||||
|
|
||||||
### Features
|
### 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
|
- Animal names instead of random numbers for pasta identifiers
|
||||||
- Automatically expiring pastas
|
- Automatically expiring pastas
|
||||||
- Never expiring pastas
|
- Never expiring pastas
|
||||||
- Listing and manually removing pastas
|
- Listing and manually removing pastas
|
||||||
|
- URL shortening and redirection
|
||||||
|
|
||||||
### Needed improvements
|
### Needed improvements
|
||||||
- Persisting pastas on disk (currently they are lost on restart)
|
- Persisting pastas on disk (currently they are lost on restart)
|
||||||
- Removing pasta after N reads
|
- Removing pasta after N reads
|
||||||
- File uploads
|
- File uploads
|
||||||
- URL shortening
|
- ~~URL shortening~~ (added on 23 April 2022)
|
||||||
|
|
||||||
|
|
BIN
git/index.png
BIN
git/index.png
Binary file not shown.
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 625 KiB |
121
src/main.rs
121
src/main.rs
|
@ -1,25 +1,34 @@
|
||||||
extern crate core;
|
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::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
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::animalnumbers::{to_animal_names, to_u64};
|
||||||
use crate::pasta::{Pasta, PastaFormData};
|
use crate::pasta::{Pasta, PastaFormData};
|
||||||
|
|
||||||
mod pasta;
|
|
||||||
mod animalnumbers;
|
mod animalnumbers;
|
||||||
|
mod pasta;
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
pastas: Mutex<Vec<Pasta>>,
|
pastas: Mutex<Vec<Pasta>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
#[clap(short, long, default_value_t = 8080)]
|
||||||
|
port: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct IndexTemplate {}
|
struct IndexTemplate {}
|
||||||
|
@ -38,42 +47,53 @@ struct PastaListTemplate<'a> {
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index() -> impl Responder {
|
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")]
|
#[post("/create")]
|
||||||
async fn create(data: web::Data<AppState>, pasta: web::Form<PastaFormData>) -> impl Responder {
|
async fn create(data: web::Data<AppState>, pasta: web::Form<PastaFormData>) -> 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) {
|
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(n) => n.as_secs(),
|
Ok(n) => n.as_secs(),
|
||||||
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||||
} as i64;
|
} as i64;
|
||||||
|
|
||||||
let expiration = match innerPasta.expiration.as_str() {
|
let expiration = match inner_pasta.expiration.as_str() {
|
||||||
"1min" => timenow + 60,
|
"1min" => timenow + 60,
|
||||||
"10min" => timenow + 60 * 10,
|
"10min" => timenow + 60 * 10,
|
||||||
"1hour" => timenow + 60 * 60,
|
"1hour" => timenow + 60 * 60,
|
||||||
"24hour" => timenow + 60 * 60 * 24,
|
"24hour" => timenow + 60 * 60 * 24,
|
||||||
"1week" => timenow + 60 * 60 * 24 * 7,
|
"1week" => timenow + 60 * 60 * 24 * 7,
|
||||||
"never" => 0,
|
"never" => 0,
|
||||||
_ => panic!("Unexpected expiration time!")
|
_ => panic!("Unexpected expiration time!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut newPasta = Pasta {
|
let pasta_type = if is_valid_url(inner_pasta.content.as_str()) {
|
||||||
|
String::from("url")
|
||||||
|
} else {
|
||||||
|
String::from("text")
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_pasta = Pasta {
|
||||||
id: rand::thread_rng().gen::<u16>() as u64,
|
id: rand::thread_rng().gen::<u16>() as u64,
|
||||||
content: innerPasta.content,
|
content: inner_pasta.content,
|
||||||
created: timenow,
|
created: timenow,
|
||||||
|
pasta_type,
|
||||||
expiration,
|
expiration,
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = newPasta.id;
|
let id = new_pasta.id;
|
||||||
|
|
||||||
pastas.push(newPasta);
|
pastas.push(new_pasta);
|
||||||
|
|
||||||
HttpResponse::Found().append_header(("Location", format!("/pasta/{}", to_animal_names(id)))).finish()
|
HttpResponse::Found()
|
||||||
|
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/pasta/{id}")]
|
#[get("/pasta/{id}")]
|
||||||
|
@ -85,14 +105,38 @@ async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpRespo
|
||||||
|
|
||||||
for pasta in pastas.iter() {
|
for pasta in pastas.iter() {
|
||||||
if pasta.id == id {
|
if pasta.id == id {
|
||||||
return HttpResponse::Found().content_type("text/html").body(PastaTemplate { pasta }.render().unwrap());
|
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<AppState>, id: web::Path<String>) -> 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<AppState>, id: web::Path<String>) -> String {
|
async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
|
||||||
let mut pastas = data.pastas.lock().unwrap();
|
let mut pastas = data.pastas.lock().unwrap();
|
||||||
let id = to_u64(&*id.into_inner());
|
let id = to_u64(&*id.into_inner());
|
||||||
|
@ -118,7 +162,9 @@ async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRespons
|
||||||
for (i, pasta) in pastas.iter().enumerate() {
|
for (i, pasta) in pastas.iter().enumerate() {
|
||||||
if pasta.id == id {
|
if pasta.id == id {
|
||||||
pastas.remove(i);
|
pastas.remove(i);
|
||||||
return HttpResponse::Found().append_header(("Location", "/pastalist")).finish();
|
return HttpResponse::Found()
|
||||||
|
.append_header(("Location", "/pastalist"))
|
||||||
|
.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,17 +177,38 @@ async fn list(data: web::Data<AppState>) -> HttpResponse {
|
||||||
|
|
||||||
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]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("Listening on http://127.0.0.1:{}", args.port.to_string())
|
||||||
|
);
|
||||||
|
|
||||||
let data = web::Data::new(AppState {
|
let data = web::Data::new(AppState {
|
||||||
pastas: Mutex::new(Vec::new()),
|
pastas: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpServer::new(move || App::new().app_data(data.clone()).service(index).service(create).service(getpasta).service(getrawpasta).service(remove).service(list)
|
HttpServer::new(move || {
|
||||||
).bind("127.0.0.1:8080")?.run().await
|
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<Pasta>) {
|
fn remove_expired(pastas: &mut Vec<Pasta>) {
|
||||||
|
@ -150,7 +217,11 @@ fn remove_expired(pastas: &mut Vec<Pasta>) {
|
||||||
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||||
} as i64;
|
} as i64;
|
||||||
|
|
||||||
pastas.retain(|p| {
|
pastas.retain(|p| p.expiration == 0 || p.expiration > timenow);
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ pub struct Pasta {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub created: i64,
|
pub created: i64,
|
||||||
pub expiration: i64
|
pub expiration: i64,
|
||||||
|
pub pasta_type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -3,22 +3,26 @@
|
||||||
<title>MicroBin</title>
|
<title>MicroBin</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<body style="max-width: 720px;
|
<link rel="stylesheet" href="/static/water.css">
|
||||||
|
</head>
|
||||||
|
<body style="
|
||||||
|
max-width: 720px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-left:0.5rem;
|
padding-left:0.5rem;
|
||||||
padding-right:0.5rem;
|
padding-right:0.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 1.1em;">
|
font-size: 1.1em;
|
||||||
|
">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<b style="margin-right: 0.5rem">
|
<b style="margin-right: 0.5rem">
|
||||||
MicroBin
|
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
|
||||||
</b>
|
</b>
|
||||||
|
|
|
||||||
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
|
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
|
||||||
|
|
|
||||||
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
|
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
|
||||||
|
|
|
||||||
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
|
@ -1,6 +1,6 @@
|
||||||
{% include "header.html" %}
|
{% include "header.html" %}
|
||||||
<form action="create" method="POST">
|
<form action="create" method="POST">
|
||||||
<input name="id" value="0" type="hidden">
|
<br>
|
||||||
<label for="expiration">Expiration</label><br>
|
<label for="expiration">Expiration</label><br>
|
||||||
<select name="expiration" id="expiration">
|
<select name="expiration" id="expiration">
|
||||||
<optgroup label="Expire">
|
<optgroup label="Expire">
|
||||||
|
@ -15,9 +15,9 @@
|
||||||
<br>
|
<br>
|
||||||
<label>Content</label>
|
<label>Content</label>
|
||||||
<br>
|
<br>
|
||||||
<textarea style="width: 100%; min-height: 100px" name="content"></textarea>
|
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
|
||||||
<br>
|
|
||||||
<br>
|
<br>
|
||||||
<input style="width: 100px; background-color: limegreen"; type="submit" value="Save"/>
|
<input style="width: 100px; background-color: limegreen"; type="submit" value="Save"/>
|
||||||
|
<br>
|
||||||
</form>
|
</form>
|
||||||
{% include "footer.html" %}
|
{% include "footer.html" %}
|
|
@ -1,6 +1,4 @@
|
||||||
{% include "header.html" %}
|
{% include "header.html" %}
|
||||||
<a href="/rawpasta/{{pasta.idAsAnimals()}}">Raw Pasta</a>
|
<a href="/raw/{{pasta.idAsAnimals()}}">Raw Pasta</a>
|
||||||
<pre style="background: lightgray; border: 1px black solid; padding: 0.5rem; overflow: auto">
|
<pre><code>{{pasta}}</code></pre>
|
||||||
{{pasta}}
|
|
||||||
</pre>
|
|
||||||
{% include "footer.html" %}
|
{% include "footer.html" %}
|
|
@ -6,8 +6,13 @@
|
||||||
No pastas yet. 😔 Create one <a href="/">here</a>.
|
No pastas yet. 😔 Create one <a href="/">here</a>.
|
||||||
</p>
|
</p>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<table style="width: 100%" border="1">
|
<br>
|
||||||
<tr style="background: lightgrey">
|
<table style="width: 100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="4">Pastas</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
Key
|
Key
|
||||||
</th>
|
</th>
|
||||||
|
@ -21,7 +26,10 @@
|
||||||
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for pasta in pastas %}
|
{% for pasta in pastas %}
|
||||||
|
{% if pasta.pasta_type == "text" %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="/pasta/{{pasta.idAsAnimals()}}">{{pasta.idAsAnimals()}}</a>
|
<a href="/pasta/{{pasta.idAsAnimals()}}">{{pasta.idAsAnimals()}}</a>
|
||||||
|
@ -33,10 +41,56 @@
|
||||||
{{pasta.expirationAsString()}}
|
{{pasta.expirationAsString()}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||||
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{%- endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<br>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="4">URL Redirects</th>
|
||||||
|
</tr>
|
||||||
|
<tr >
|
||||||
|
<th>
|
||||||
|
Key
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Expiration
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for pasta in pastas %}
|
||||||
|
{% if pasta.pasta_type == "url" %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/url/{{pasta.idAsAnimals()}}">{{pasta.idAsAnimals()}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{pasta.createdAsString()}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{pasta.expirationAsString()}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a style="margin-right:1rem" href="/raw/{{pasta.idAsAnimals()}}">Raw</a>
|
||||||
|
<a href="/remove/{{pasta.idAsAnimals()}}">Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{%- endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% include "footer.html" %}
|
{% include "footer.html" %}
|
Loading…
Reference in a new issue