init
This commit is contained in:
commit
3f461e8375
6 changed files with 1571 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/store
|
1185
Cargo.lock
generated
Normal file
1185
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "motifs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
axum = "0.7.5"
|
||||
chrono = "0.4.38"
|
||||
maud = { version = "0.26.0", features = ["axum"] }
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_bson = "0.0.1"
|
||||
serde_json = "1.0.120"
|
||||
sled = "0.34.7"
|
||||
stylist = "0.13.0"
|
||||
tokio = { version = "1.39.1", features = ["net", "rt-multi-thread"] }
|
91
src/common.rs
Normal file
91
src/common.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use axum::response::IntoResponse;
|
||||
use maud::{html, Markup};
|
||||
use stylist::{css, GlobalStyle};
|
||||
|
||||
pub fn head(title: &str) -> Markup {
|
||||
html!(
|
||||
head {
|
||||
title { "Recueil " (title) }
|
||||
link rel = "stylesheet" href = "/style.css";
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn header() -> Markup {
|
||||
html!(
|
||||
header {
|
||||
a href = "/" { h1 { "Motifs" } }
|
||||
a href = "/activity" { "activité" }
|
||||
a href = "/topics" { "sujets" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn footer() -> Markup {
|
||||
html!(
|
||||
footer {
|
||||
a href = "https://barnulf.net" { "barnulf.net" }
|
||||
p { "Propulsé par la rouille." }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn style() -> impl IntoResponse {
|
||||
#[allow(non_upper_case_globals)]
|
||||
let style = GlobalStyle::new(css!(
|
||||
page, html, body, content {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #181818;
|
||||
color: white;
|
||||
display: grid;
|
||||
place-items: start center;
|
||||
}
|
||||
|
||||
content {
|
||||
background-color: #202020;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
border-left: 1px solid #ffffff16;
|
||||
border-right: 1px solid #ffffff16;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid #ffffff16;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-left: 2rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: wheat;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid #ffffff16;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-left: 2rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
))
|
||||
.expect("Fails to compile style.")
|
||||
.get_style_str()
|
||||
.to_string();
|
||||
|
||||
([("content-type", "style/css")], style)
|
||||
}
|
210
src/main.rs
Normal file
210
src/main.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use std::fmt::Write;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::prelude::*;
|
||||
use common::{footer, head, header, style};
|
||||
use maud::{html, Escaper, Markup, PreEscaped};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use store::Store;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
mod common;
|
||||
mod store;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MainState {
|
||||
store: Arc<RwLock<Store>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let store = Store::open("./store")?;
|
||||
let store = Arc::new(RwLock::new(store));
|
||||
let state = MainState { store };
|
||||
|
||||
let app = Router::new()
|
||||
.route("/style.css", get(style))
|
||||
.route("/topics", get(topics).post(post))
|
||||
.route("/topic/:name", get(topic))
|
||||
.route("/activity", get(activity))
|
||||
.route("/", get(home))
|
||||
.with_state(state);
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
let server = axum::serve(listener, app);
|
||||
server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn home() -> Markup {
|
||||
html!(
|
||||
(head(""))
|
||||
body {
|
||||
content {
|
||||
(header())
|
||||
main {
|
||||
"Bienvenue au recueil de Barnulf."
|
||||
br;
|
||||
"
|
||||
Ce site contient une collection ouverte de réflexions
|
||||
personnelles sur différents sujets relatifs à l'informatique.
|
||||
"
|
||||
}
|
||||
(footer())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn topics(State(state): State<MainState>) -> impl IntoResponse {
|
||||
let topics = state.store.read().unwrap().list().unwrap();
|
||||
html!(
|
||||
(head("sujets"))
|
||||
body {
|
||||
content {
|
||||
(header())
|
||||
main {
|
||||
h2 { "Sujets" }
|
||||
ul {
|
||||
@for topic in topics {
|
||||
li {
|
||||
a href = { "/topic/" (topic) } { (topic) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn activity(State(state): State<MainState>) -> impl IntoResponse {
|
||||
let topics = state.store.read().unwrap().activity().unwrap();
|
||||
html!(
|
||||
(head("activité"))
|
||||
body {
|
||||
content {
|
||||
(header())
|
||||
main {
|
||||
h2 { "Activité" }
|
||||
ul {
|
||||
@for topic in topics {
|
||||
li {
|
||||
a href = { "/topic/" (topic) } { (topic) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn topic(Path(name): Path<String>, State(state): State<MainState>) -> impl IntoResponse {
|
||||
let Ok(posts) = state.store.read().unwrap().get(&name) else {
|
||||
return error("No such topic.");
|
||||
};
|
||||
|
||||
html!(
|
||||
(head(&name))
|
||||
body {
|
||||
content {
|
||||
(header())
|
||||
main {
|
||||
h2 { (name) }
|
||||
|
||||
@for post in posts {
|
||||
hr;
|
||||
section { (PreEscaped({
|
||||
let mut buffer = String::new();
|
||||
write!(Escaper::new(&mut buffer), "{post}").unwrap();
|
||||
buffer.replace('\n', "<br>")
|
||||
})) }
|
||||
}
|
||||
}
|
||||
(footer())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PostContent {
|
||||
topic: String,
|
||||
content: String,
|
||||
author: String,
|
||||
}
|
||||
|
||||
async fn post(
|
||||
State(state): State<MainState>,
|
||||
Json(PostContent {
|
||||
topic,
|
||||
content,
|
||||
author,
|
||||
}): Json<PostContent>,
|
||||
) -> impl IntoResponse {
|
||||
let mut store = state.store.write().unwrap();
|
||||
let date = Utc::now().format("%d/%m/%Y");
|
||||
let topic = sanithize_identifier(&topic);
|
||||
let content = content.trim();
|
||||
let content = format!("{content}\n\t~{author}, {date}");
|
||||
store.insert(&topic, content).unwrap();
|
||||
}
|
||||
|
||||
fn sanithize_identifier(input: &str) -> String {
|
||||
let text = input.to_lowercase();
|
||||
let replaces = [
|
||||
('à', 'a'),
|
||||
('â', 'a'),
|
||||
('ä', 'a'),
|
||||
('é', 'e'),
|
||||
('è', 'e'),
|
||||
('ê', 'e'),
|
||||
('ë', 'e'),
|
||||
('î', 'i'),
|
||||
('ï', 'i'),
|
||||
('ô', 'o'),
|
||||
('ö', 'o'),
|
||||
('û', 'u'),
|
||||
('ü', 'u'),
|
||||
];
|
||||
text.chars()
|
||||
.map(|c| {
|
||||
for (from, to) in replaces {
|
||||
if c == from {
|
||||
return to;
|
||||
}
|
||||
}
|
||||
c
|
||||
})
|
||||
.filter(char::is_ascii)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn error(message: impl ToString) -> Markup {
|
||||
let message = message.to_string();
|
||||
html!(
|
||||
(head("Failure"))
|
||||
body {
|
||||
content {
|
||||
(header())
|
||||
main {
|
||||
h2 { "Failure" }
|
||||
br;
|
||||
(message)
|
||||
}
|
||||
(footer())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
67
src/store.rs
Normal file
67
src/store.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::{ErrorKind, Write},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn open(path: impl ToString) -> Result<Self> {
|
||||
let path = path.to_string();
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<Vec<String>> {
|
||||
let mut results = Vec::new();
|
||||
for result in fs::read_dir(&self.path)? {
|
||||
let entry = result?;
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
results.push(name);
|
||||
}
|
||||
results.sort();
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn activity(&self) -> Result<Vec<String>> {
|
||||
let mut results = Vec::new();
|
||||
for result in fs::read_dir(&self.path)? {
|
||||
let entry = result?;
|
||||
let name = entry.file_name().into_string().unwrap();
|
||||
let modif_time = entry.metadata()?.modified()?;
|
||||
let duration = modif_time.duration_since(std::time::UNIX_EPOCH)?;
|
||||
let date = duration.as_nanos();
|
||||
results.push((name, date));
|
||||
}
|
||||
|
||||
results.sort_by_key(|&(_, date)| date);
|
||||
Ok(results.into_iter().rev().map(|(name, _)| name).collect())
|
||||
}
|
||||
|
||||
pub fn get(&self, topic: &str) -> Result<Vec<String>> {
|
||||
let base = &self.path;
|
||||
let path = format!("{base}/{topic}");
|
||||
let content = fs::read_to_string(path)?;
|
||||
Ok(content.split(SEPARATOR).map(String::from).collect())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, topic: &str, content: String) -> Result<()> {
|
||||
let content = content.replace('\u{c}', " ");
|
||||
let content = content.trim();
|
||||
let base = &self.path;
|
||||
let path = format!("{base}/{topic}");
|
||||
match OpenOptions::new().append(true).create(false).open(&path) {
|
||||
Ok(mut file) => write!(&mut file, "{SEPARATOR}\n{content}\n")?,
|
||||
Err(error) if error.kind() == ErrorKind::NotFound => drop(fs::write(path, content)),
|
||||
Err(error) => Err(error)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const SEPARATOR: &str = "\n\u{c}\n";
|
Loading…
Add table
Add a link
Reference in a new issue