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:
parent
fc63b7aa1a
commit
fe926086c2
4 changed files with 150 additions and 35 deletions
19
src/dbio.rs
19
src/dbio.rs
|
@ -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(_) => {
|
||||||
|
|
151
src/main.rs
151
src/main.rs
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue