CLI Args extension and file management fix

- added cli param to change threads used by server
- added cli param hide html header and/or footer
- added cli param to remove css styling
- added cli param to set http basic auth username and optionally password for the server
- file pastas now delete the files from the disk as well when they expire
This commit is contained in:
Daniel Szabo 2022-05-07 22:30:57 +01:00
parent fc63b7aa1a
commit fe926086c2
4 changed files with 150 additions and 35 deletions

View file

@ -1,11 +1,8 @@
use std::fs::File; use std::fs::File;
use std::io::{BufReader, BufWriter, Error}; use std::io;
use std::{fmt, io}; use std::io::{BufReader, BufWriter};
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc}; use crate::Pasta;
use log::log;
use crate::{to_animal_names, Pasta};
static DATABASE_PATH: &'static str = "pasta_data/database.json"; static DATABASE_PATH: &'static str = "pasta_data/database.json";
@ -13,8 +10,8 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
let mut file = File::create(DATABASE_PATH); let mut file = File::create(DATABASE_PATH);
match file { match file {
Ok(_) => { Ok(_) => {
let mut writer = BufWriter::new(file.unwrap()); let writer = BufWriter::new(file.unwrap());
serde_json::to_writer(&mut writer, &pasta_data); serde_json::to_writer(writer, &pasta_data).expect("Failed to create JSON writer");
} }
Err(_) => { Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH); log::info!("Database file {} not found!", DATABASE_PATH);
@ -38,11 +35,11 @@ pub fn save_to_file(pasta_data: &Vec<Pasta>) {
} }
pub fn load_from_file() -> io::Result<Vec<Pasta>> { pub fn load_from_file() -> io::Result<Vec<Pasta>> {
let mut file = File::open(DATABASE_PATH); let file = File::open(DATABASE_PATH);
match file { match file {
Ok(_) => { Ok(_) => {
let mut reader = BufReader::new(file.unwrap()); let reader = BufReader::new(file.unwrap());
let data: Vec<Pasta> = serde_json::from_reader(&mut reader).unwrap(); let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap();
Ok(data) Ok(data)
} }
Err(_) => { Err(_) => {

View file

@ -5,16 +5,22 @@ use std::io::Write;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use actix_files as fs; use actix_files;
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{get, web, App, Error, HttpResponse, HttpServer, Responder}; use actix_web::dev::ServiceRequest;
use actix_web::middleware::Condition;
use actix_web::{error, get, middleware, web, App, Error, HttpResponse, HttpServer, Responder};
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
use askama::Template; use askama::Template;
use chrono::Local; use chrono::Local;
use clap::Parser; use clap::Parser;
use futures::TryStreamExt as _; use futures::TryStreamExt as _;
use lazy_static::lazy_static;
use linkify::{LinkFinder, LinkKind}; use linkify::{LinkFinder, LinkKind};
use log::LevelFilter; use log::LevelFilter;
use rand::Rng; use rand::Rng;
use std::fs;
use crate::animalnumbers::{to_animal_names, to_u64}; use crate::animalnumbers::{to_animal_names, to_u64};
use crate::dbio::save_to_file; use crate::dbio::save_to_file;
@ -24,48 +30,103 @@ mod animalnumbers;
mod dbio; mod dbio;
mod pasta; mod pasta;
lazy_static! {
static ref ARGS: Args = Args::parse();
}
struct AppState { struct AppState {
pastas: Mutex<Vec<Pasta>>, pastas: Mutex<Vec<Pasta>>,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
struct Args { struct Args {
#[clap(short, long, default_value_t = 8080)] #[clap(short, long, default_value_t = 8080)]
port: u32, port: u32,
#[clap(short, long, default_value_t = 1)]
threads: u8,
#[clap(long)]
hide_header: bool,
#[clap(long)]
hide_footer: bool,
#[clap(long)]
pure_html: bool,
#[clap(long)]
no_listing: bool,
#[clap(long)]
auth_username: Option<String>,
#[clap(long)]
auth_password: Option<String>,
}
async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_password.as_ref() {
Some(cred_pass) => match credentials.password() {
None => Err(error::ErrorBadRequest("Invalid login details.")),
Some(arg_pass) => {
if arg_pass == cred_pass {
Ok(req)
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
}
},
None => Ok(req),
};
} else {
Err(error::ErrorBadRequest("Invalid login details."))
}
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate {} struct IndexTemplate<'a> {
args: &'a Args,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "error.html")] #[template(path = "error.html")]
struct ErrorTemplate {} struct ErrorTemplate<'a> {
args: &'a Args,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "pasta.html")] #[template(path = "pasta.html")]
struct PastaTemplate<'a> { struct PastaTemplate<'a> {
pasta: &'a Pasta, pasta: &'a Pasta,
args: &'a Args,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "pastalist.html")] #[template(path = "pastalist.html")]
struct PastaListTemplate<'a> { struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>, pastas: &'a Vec<Pasta>,
args: &'a Args,
} }
#[get("/")] #[get("/")]
async fn index() -> impl Responder { async fn index() -> impl Responder {
HttpResponse::Found() HttpResponse::Found()
.content_type("text/html") .content_type("text/html")
.body(IndexTemplate {}.render().unwrap()) .body(IndexTemplate { args: &ARGS }.render().unwrap())
} }
async fn not_found() -> Result<HttpResponse, Error> { async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.content_type("text/html") .content_type("text/html")
.body(ErrorTemplate {}.render().unwrap())) .body(ErrorTemplate { args: &ARGS }.render().unwrap()))
} }
async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> { async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> {
@ -155,6 +216,7 @@ async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<Htt
#[get("/pasta/{id}")] #[get("/pasta/{id}")]
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse { async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
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());
remove_expired(&mut pastas); remove_expired(&mut pastas);
@ -163,18 +225,19 @@ async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpRespo
if pasta.id == id { if pasta.id == id {
return HttpResponse::Found() return HttpResponse::Found()
.content_type("text/html") .content_type("text/html")
.body(PastaTemplate { pasta }.render().unwrap()); .body(PastaTemplate { pasta, args: &ARGS }.render().unwrap());
} }
} }
HttpResponse::Found() HttpResponse::Found()
.content_type("text/html") .content_type("text/html")
.body(ErrorTemplate {}.render().unwrap()) .body(ErrorTemplate { args: &ARGS }.render().unwrap())
} }
#[get("/url/{id}")] #[get("/url/{id}")]
async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse { async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
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());
remove_expired(&mut pastas); remove_expired(&mut pastas);
@ -186,12 +249,16 @@ async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpRe
.append_header(("Location", String::from(&pasta.content))) .append_header(("Location", String::from(&pasta.content)))
.finish(); .finish();
} else { } else {
return HttpResponse::Found().body("This is not a valid URL. :-("); return HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
} }
} }
} }
HttpResponse::Found().body("Pasta not found! :-(") HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
} }
#[get("/raw/{id}")] #[get("/raw/{id}")]
@ -214,6 +281,7 @@ async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String
#[get("/remove/{id}")] #[get("/remove/{id}")]
async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse { async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
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());
remove_expired(&mut pastas); remove_expired(&mut pastas);
@ -226,23 +294,37 @@ async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpRespons
.finish(); .finish();
} }
} }
HttpResponse::Found().body("Pasta not found! :-(")
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
} }
#[get("/pastalist")] #[get("/pastalist")]
async fn list(data: web::Data<AppState>) -> HttpResponse { async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap(); let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas); remove_expired(&mut pastas);
HttpResponse::Found() HttpResponse::Found().content_type("text/html").body(
.content_type("text/html") PastaListTemplate {
.body(PastaListTemplate { pastas: &pastas }.render().unwrap()) pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
} }
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let args = Args::parse(); let args: Args = Args::parse();
Builder::new() Builder::new()
.format(|buf, record| { .format(|buf, record| {
@ -258,11 +340,17 @@ async fn main() -> std::io::Result<()> {
.init(); .init();
log::info!( log::info!(
"MicroBin listening on http://127.0.0.1:{}", "MicroBin starting on http://127.0.0.1:{}",
args.port.to_string() args.port.to_string()
); );
std::fs::create_dir_all("./pasta_data").unwrap(); match std::fs::create_dir_all("./pasta_data") {
Ok(dir) => dir,
Err(error) => {
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
panic!("Couldn't create data directory ./pasta_data: {:?}", error);
}
};
let data = web::Data::new(AppState { let data = web::Data::new(AppState {
pastas: Mutex::new(dbio::load_from_file().unwrap()), pastas: Mutex::new(dbio::load_from_file().unwrap()),
@ -271,16 +359,22 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(data.clone()) .app_data(data.clone())
.wrap(middleware::NormalizePath::trim())
.service(index) .service(index)
.service(getpasta) .service(getpasta)
.service(redirecturl) .service(redirecturl)
.service(getrawpasta) .service(getrawpasta)
.service(remove) .service(actix_files::Files::new("/static", "./static"))
.service(list) .service(actix_files::Files::new("/file", "./pasta_data"))
.service(fs::Files::new("/static", "./static"))
.service(fs::Files::new("/file", "./pasta_data"))
.service(web::resource("/upload").route(web::post().to(create))) .service(web::resource("/upload").route(web::post().to(create)))
.default_service(web::route().to(not_found)) .default_service(web::route().to(not_found))
.wrap(middleware::Logger::default())
.service(remove)
.service(list)
.wrap(Condition::new(
args.auth_username.is_some(),
HttpAuthentication::basic(auth_validator),
))
}) })
.bind(format!("127.0.0.1:{}", args.port.to_string()))? .bind(format!("127.0.0.1:{}", args.port.to_string()))?
.run() .run()
@ -288,12 +382,23 @@ async fn main() -> std::io::Result<()> {
} }
fn remove_expired(pastas: &mut Vec<Pasta>) { fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
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;
pastas.retain(|p| p.expiration == 0 || p.expiration > timenow); pastas.retain(|p| {
// delete the files too
if p.expiration < timenow {
// remove the file itself
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file));
// and remove the containing directory
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals()));
};
p.expiration == 0 || p.expiration > timenow
});
} }
fn is_valid_url(url: &str) -> bool { fn is_valid_url(url: &str) -> bool {

View file

@ -1,7 +1,13 @@
{% if !args.hide_footer %}
<hr> <hr>
<p style="font-size: smaller"> <p style="font-size: smaller">
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>! MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>! Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
</p> </p>
{%- endif %}
</body> </body>
</html> </html>

View file

@ -3,7 +3,9 @@
<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">
{% if !args.pure_html %}
<link rel="stylesheet" href="/static/water.css"> <link rel="stylesheet" href="/static/water.css">
{%- endif %}
</head> </head>
<body style=" <body style="
max-width: 720px; max-width: 720px;
@ -13,8 +15,11 @@
line-height: 1.5; line-height: 1.5;
font-size: 1.1em; font-size: 1.1em;
"> ">
<br> <br>
{% if !args.hide_header %}
<b style="margin-right: 0.5rem"> <b style="margin-right: 0.5rem">
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin <i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
</b> </b>
@ -26,3 +31,5 @@
<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>
{%- endif %}