Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
df6720bd66 | |||
b039753181 | |||
a07c61fdbf | |||
5a021f9781 | |||
86ffc13319 |
4 changed files with 52 additions and 20 deletions
|
@ -1,9 +1,10 @@
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup, DOCTYPE};
|
||||||
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";
|
||||||
|
@ -71,6 +72,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,7 +14,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, DOCTYPE};
|
use maud::{html, Escaper, Markup, PreEscaped};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use store::Store;
|
use store::Store;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
@ -22,6 +23,22 @@ 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>>,
|
||||||
|
@ -29,7 +46,13 @@ struct MainState {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let store = Store::open("./store")?;
|
let Cmd {
|
||||||
|
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 };
|
||||||
|
|
||||||
|
@ -42,8 +65,10 @@ async fn main() -> Result<()> {
|
||||||
.route("/", get(home))
|
.route("/", get(home))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = TcpListener::bind((address.as_str(), port)).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(())
|
||||||
|
@ -51,7 +76,6 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
async fn home() -> Markup {
|
async fn home() -> Markup {
|
||||||
html!(
|
html!(
|
||||||
(DOCTYPE)
|
|
||||||
(head(""))
|
(head(""))
|
||||||
body {
|
body {
|
||||||
content {
|
content {
|
||||||
|
@ -73,7 +97,6 @@ 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 {
|
||||||
|
@ -97,7 +120,6 @@ 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 {
|
||||||
|
@ -124,7 +146,6 @@ async fn topic(Path(name): Path<String>, State(state): State<MainState>) -> impl
|
||||||
};
|
};
|
||||||
|
|
||||||
html!(
|
html!(
|
||||||
(DOCTYPE)
|
|
||||||
(head(&name))
|
(head(&name))
|
||||||
body {
|
body {
|
||||||
content {
|
content {
|
||||||
|
@ -164,11 +185,11 @@ fn validate_topic(topic: &str) -> Result<(), ValidationError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post(State(state): State<MainState>, Form(post_content): Form<PostContent>) -> Response {
|
async fn post(State(state): State<MainState>, Form(post): 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_content.validate().is_err() {
|
if post.validate().is_err() {
|
||||||
return (StatusCode::BAD_REQUEST, "Bad input.").into_response();
|
return (StatusCode::BAD_REQUEST, "Bad input.").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +197,7 @@ async fn post(State(state): State<MainState>, Form(post_content): Form<PostConte
|
||||||
topic,
|
topic,
|
||||||
content,
|
content,
|
||||||
author,
|
author,
|
||||||
} = post_content;
|
} = post;
|
||||||
|
|
||||||
let topic = sanithize_identifier(&topic);
|
let topic = sanithize_identifier(&topic);
|
||||||
|
|
||||||
|
@ -218,7 +239,6 @@ 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 {
|
||||||
|
@ -251,7 +271,6 @@ 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 {
|
||||||
|
|
12
src/store.rs
12
src/store.rs
|
@ -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: String,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub fn open(path: impl ToString) -> Result<Self> {
|
pub fn open(path: PathBuf) -> Result<Self> {
|
||||||
let path = path.to_string();
|
|
||||||
fs::create_dir_all(&path)?;
|
fs::create_dir_all(&path)?;
|
||||||
Ok(Self { path })
|
Ok(Self { path })
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,7 @@ impl Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, topic: &str) -> Result<Vec<String>> {
|
pub fn get(&self, topic: &str) -> Result<Vec<String>> {
|
||||||
let base = &self.path;
|
let path = self.path.join(topic);
|
||||||
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())
|
||||||
}
|
}
|
||||||
|
@ -53,8 +52,7 @@ 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 base = &self.path;
|
let path = self.path.join(topic);
|
||||||
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
Executable file
3
watch
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cargo watch --clear --exec "run -- --address=localhost --port=8080"
|
Loading…
Add table
Add a link
Reference in a new issue