Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

4 changed files with 20 additions and 52 deletions

View file

@ -1,10 +1,9 @@
use axum::response::IntoResponse; use axum::response::IntoResponse;
use maud::{html, Markup, DOCTYPE}; use maud::{html, Markup};
use stylist::{css, GlobalStyle}; use stylist::{css, GlobalStyle};
pub fn head(title: &str) -> Markup { pub fn head(title: &str) -> Markup {
html!( html!(
(DOCTYPE)
head { head {
title { "Recueil " (title) } title { "Recueil " (title) }
link rel = "stylesheet" href = "/style.css"; link rel = "stylesheet" href = "/style.css";
@ -72,17 +71,6 @@ pub async fn style() -> impl IntoResponse {
color: wheat; color: wheat;
} }
label {
display: inline-block;
width: 8rem;
}
textarea {
width: 100%;
height: 5rem;
box-sizing: border-box;
}
main { main {
padding: 2rem; padding: 2rem;
} }

View file

@ -1,6 +1,5 @@
use std::{ use std::{
fmt::Write, fmt::Write,
path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
@ -14,7 +13,7 @@ use axum::{
}; };
use chrono::prelude::*; use chrono::prelude::*;
use common::{footer, head, header, style}; use common::{footer, head, header, style};
use maud::{html, Escaper, Markup, PreEscaped}; use maud::{html, Escaper, Markup, PreEscaped, DOCTYPE};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use store::Store; use store::Store;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -23,22 +22,6 @@ use validator::{Validate, ValidationError};
mod common; mod common;
mod store; mod store;
#[derive(Debug, clap::Parser)]
/// Arbre
struct Cmd {
#[arg(short, long, default_value_t = 8200)]
/// Port on which th server will listen.
port: u16,
#[arg(short, long, default_value = "0.0.0.0")]
/// Hostname or address on which th server will listen.
address: String,
#[arg(short, long, default_value = "./store")]
/// Path to the directory to use as storage for topics.
store: PathBuf,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct MainState { struct MainState {
store: Arc<RwLock<Store>>, store: Arc<RwLock<Store>>,
@ -46,13 +29,7 @@ struct MainState {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let Cmd { let store = Store::open("./store")?;
address,
port,
store,
} = clap::Parser::parse();
let store = Store::open(store)?;
let store = Arc::new(RwLock::new(store)); let store = Arc::new(RwLock::new(store));
let state = MainState { store }; let state = MainState { store };
@ -65,10 +42,8 @@ async fn main() -> Result<()> {
.route("/", get(home)) .route("/", get(home))
.with_state(state); .with_state(state);
let listener = TcpListener::bind((address.as_str(), port)).await.unwrap(); let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
let server = axum::serve(listener, app); let server = axum::serve(listener, app);
println!("Listening on http://{address}:{port}");
server.await?; server.await?;
Ok(()) Ok(())
@ -76,6 +51,7 @@ async fn main() -> Result<()> {
async fn home() -> Markup { async fn home() -> Markup {
html!( html!(
(DOCTYPE)
(head("")) (head(""))
body { body {
content { content {
@ -97,6 +73,7 @@ async fn home() -> Markup {
async fn topics(State(state): State<MainState>) -> impl IntoResponse { async fn topics(State(state): State<MainState>) -> impl IntoResponse {
let topics = state.store.read().unwrap().list().unwrap(); let topics = state.store.read().unwrap().list().unwrap();
html!( html!(
(DOCTYPE)
(head("sujets")) (head("sujets"))
body { body {
content { content {
@ -120,6 +97,7 @@ async fn topics(State(state): State<MainState>) -> impl IntoResponse {
async fn activity(State(state): State<MainState>) -> impl IntoResponse { async fn activity(State(state): State<MainState>) -> impl IntoResponse {
let topics = state.store.read().unwrap().activity().unwrap(); let topics = state.store.read().unwrap().activity().unwrap();
html!( html!(
(DOCTYPE)
(head("activité")) (head("activité"))
body { body {
content { content {
@ -146,6 +124,7 @@ async fn topic(Path(name): Path<String>, State(state): State<MainState>) -> impl
}; };
html!( html!(
(DOCTYPE)
(head(&name)) (head(&name))
body { body {
content { content {
@ -185,11 +164,11 @@ fn validate_topic(topic: &str) -> Result<(), ValidationError> {
Ok(()) Ok(())
} }
async fn post(State(state): State<MainState>, Form(post): Form<PostContent>) -> Response { async fn post(State(state): State<MainState>, Form(post_content): Form<PostContent>) -> Response {
let mut store = state.store.write().unwrap(); let mut store = state.store.write().unwrap();
let date = Utc::now().format("%d/%m/%Y"); let date = Utc::now().format("%d/%m/%Y");
if post.validate().is_err() { if post_content.validate().is_err() {
return (StatusCode::BAD_REQUEST, "Bad input.").into_response(); return (StatusCode::BAD_REQUEST, "Bad input.").into_response();
} }
@ -197,7 +176,7 @@ async fn post(State(state): State<MainState>, Form(post): Form<PostContent>) ->
topic, topic,
content, content,
author, author,
} = post; } = post_content;
let topic = sanithize_identifier(&topic); let topic = sanithize_identifier(&topic);
@ -239,6 +218,7 @@ fn sanithize_identifier(input: &str) -> String {
async fn create() -> impl IntoResponse { async fn create() -> impl IntoResponse {
html!( html!(
(DOCTYPE)
(head("créer")) (head("créer"))
body { body {
content { content {
@ -271,6 +251,7 @@ async fn create() -> impl IntoResponse {
fn error(message: impl ToString) -> Markup { fn error(message: impl ToString) -> Markup {
let message = message.to_string(); let message = message.to_string();
html!( html!(
(DOCTYPE)
(head("Failure")) (head("Failure"))
body { body {
content { content {

View file

@ -1,18 +1,18 @@
use std::{ use std::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io::{ErrorKind, Write}, io::{ErrorKind, Write},
path::PathBuf,
}; };
use anyhow::Result; use anyhow::Result;
#[derive(Debug)] #[derive(Debug)]
pub struct Store { pub struct Store {
path: PathBuf, path: String,
} }
impl Store { impl Store {
pub fn open(path: PathBuf) -> Result<Self> { pub fn open(path: impl ToString) -> Result<Self> {
let path = path.to_string();
fs::create_dir_all(&path)?; fs::create_dir_all(&path)?;
Ok(Self { path }) Ok(Self { path })
} }
@ -44,7 +44,8 @@ impl Store {
} }
pub fn get(&self, topic: &str) -> Result<Vec<String>> { pub fn get(&self, topic: &str) -> Result<Vec<String>> {
let path = self.path.join(topic); let base = &self.path;
let path = format!("{base}/{topic}");
let content = fs::read_to_string(path)?; let content = fs::read_to_string(path)?;
Ok(content.split(SEPARATOR).map(String::from).collect()) Ok(content.split(SEPARATOR).map(String::from).collect())
} }
@ -52,7 +53,8 @@ impl Store {
pub fn insert(&mut self, topic: &str, content: String) -> Result<()> { pub fn insert(&mut self, topic: &str, content: String) -> Result<()> {
let content = content.replace('\u{c}', " "); let content = content.replace('\u{c}', " ");
let content = content.trim(); let content = content.trim();
let path = self.path.join(topic); let base = &self.path;
let path = format!("{base}/{topic}");
match OpenOptions::new().append(true).create(false).open(&path) { match OpenOptions::new().append(true).create(false).open(&path) {
Ok(mut file) => write!(&mut file, "{SEPARATOR}\n{content}\n")?, Ok(mut file) => write!(&mut file, "{SEPARATOR}\n{content}\n")?,
Err(error) if error.kind() == ErrorKind::NotFound => drop(fs::write(path, content)), Err(error) if error.kind() == ErrorKind::NotFound => drop(fs::write(path, content)),

3
watch
View file

@ -1,3 +0,0 @@
#!/bin/sh
cargo watch --clear --exec "run -- --address=localhost --port=8080"