Compare commits

..

No commits in common. "1d993fb6cc7663841846dcfa2ee83f9bc15546e5" and "ae3bd23e4b2e8dd4d3cb939ff13ac300660863e0" have entirely different histories.

4 changed files with 22 additions and 76 deletions

2
Cargo.lock generated
View file

@ -470,7 +470,7 @@ dependencies = [
[[package]] [[package]]
name = "guestbook" name = "guestbook"
version = "0.2.8" version = "0.2.7"
dependencies = [ dependencies = [
"axum", "axum",
"base64 0.22.1", "base64 0.22.1",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "guestbook" name = "guestbook"
version = "0.2.8" version = "0.2.7"
edition = "2021" edition = "2021"
description = "A configurable, self-hosted guestbook for the web, allowing visitors to leave behind messages, drawings, and voice notes, with spam-prevention and moderation via Telegram bot." description = "A configurable, self-hosted guestbook for the web, allowing visitors to leave behind messages, drawings, and voice notes, with spam-prevention and moderation via Telegram bot."
license = "MIT" license = "MIT"

View file

@ -1,32 +1,6 @@
use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::Path; use std::path::Path;
pub fn write_atomic(path: &Path, contents: &[u8]) -> std::io::Result<()> {
let parent = path.parent().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
})?;
let filename = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "bad filename"))?;
let suffix: u32 = rand::rng().random();
let tmp = parent.join(format!(".{filename}.{suffix:08x}.tmp"));
let mut f = std::fs::File::create(&tmp)?;
f.write_all(contents)?;
f.sync_all()?;
drop(f);
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Status { pub enum Status {
@ -175,16 +149,17 @@ pub fn append_reply(dir: &Path, id: &str, reply: &str) -> Result<String, String>
entry.body.push_str("\n\n"); entry.body.push_str("\n\n");
entry.body.push_str(&quoted); entry.body.push_str(&quoted);
let path = dir.join(format!("{}.txt", entry.id)); let path = dir.join(format!("{}.txt", entry.id));
write_atomic(&path, entry.to_file_contents().as_bytes()).map_err(|e| e.to_string())?; std::fs::write(&path, entry.to_file_contents()).map_err(|e| e.to_string())?;
Ok(entry.meta.name.clone()) Ok(entry.meta.name.clone())
} }
#[cfg(any(feature = "telegram", test))] #[cfg(any(feature = "telegram", test))]
/// Find an entry file by ID and update its status.
pub fn set_status(dir: &Path, short_id: &str, status: Status) -> Result<String, String> { pub fn set_status(dir: &Path, short_id: &str, status: Status) -> Result<String, String> {
let mut entry = find_entry(dir, short_id)?; let mut entry = find_entry(dir, short_id)?;
entry.meta.status = status; entry.meta.status = status;
let path = dir.join(format!("{}.txt", entry.id)); let path = dir.join(format!("{}.txt", entry.id));
write_atomic(&path, entry.to_file_contents().as_bytes()).map_err(|e| e.to_string())?; std::fs::write(&path, entry.to_file_contents()).map_err(|e| e.to_string())?;
Ok(entry.meta.name.clone()) Ok(entry.meta.name.clone())
} }

View file

@ -49,20 +49,13 @@ pub fn router(state: Arc<AppState>) -> Router {
.with_state(state) .with_state(state)
} }
fn generate_id(entries_dir: &std::path::Path) -> std::io::Result<String> { fn generate_id(entries_dir: &std::path::Path) -> String {
const CHARS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; const CHARS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let mut rng = rand::rng(); let mut rng = rand::rng();
loop { loop {
let id: String = (0..4).map(|_| CHARS[rng.random_range(0..36)] as char).collect(); let id: String = (0..4).map(|_| CHARS[rng.random_range(0..36)] as char).collect();
let path = entries_dir.join(format!("{id}.txt")); if !entries_dir.join(format!("{id}.txt")).exists() {
match std::fs::OpenOptions::new() return id;
.write(true)
.create_new(true)
.open(&path)
{
Ok(_) => return Ok(id),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
} }
} }
} }
@ -279,33 +272,19 @@ async fn submit(
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let date = now.format("%Y-%m-%dT%H:%M:%S").to_string(); let date = now.format("%Y-%m-%dT%H:%M:%S").to_string();
let id = generate_id(&state.config.data_dir.join("entries"));
let entries_dir = state.config.data_dir.join("entries");
if let Err(e) = std::fs::create_dir_all(&entries_dir) {
tracing::error!("failed to create entries directory: {e}");
return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
}
let id = match generate_id(&entries_dir) {
Ok(id) => id,
Err(e) => {
tracing::error!("failed to reserve entry id: {e}");
return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
}
};
let filename = format!("{id}.txt"); let filename = format!("{id}.txt");
let path = entries_dir.join(&filename);
// Save drawing with the same ID as the entry
let drawing_filename = if let Some(ref bytes) = drawing_bytes { let drawing_filename = if let Some(ref bytes) = drawing_bytes {
let drawing_name = format!("{id}.png"); let drawing_name = format!("{id}.png");
let drawings_dir = state.config.data_dir.join("drawings"); let drawings_dir = state.config.data_dir.join("drawings");
if let Err(e) = std::fs::create_dir_all(&drawings_dir) { if let Err(e) = std::fs::create_dir_all(&drawings_dir) {
tracing::error!("failed to create drawings directory: {e}"); tracing::error!("failed to create drawings directory: {e}");
let _ = std::fs::remove_file(&path);
return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
} }
if let Err(e) = entries::write_atomic(&drawings_dir.join(&drawing_name), bytes) { if let Err(e) = std::fs::write(drawings_dir.join(&drawing_name), bytes) {
tracing::error!("failed to write drawing: {e}"); tracing::error!("failed to write drawing: {e}");
let _ = std::fs::remove_file(&path);
return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
} }
drawing_name drawing_name
@ -318,18 +297,10 @@ async fn submit(
let vn_dir = state.config.data_dir.join("voice_notes"); let vn_dir = state.config.data_dir.join("voice_notes");
if let Err(e) = std::fs::create_dir_all(&vn_dir) { if let Err(e) = std::fs::create_dir_all(&vn_dir) {
tracing::error!("failed to create voice notes directory: {e}"); tracing::error!("failed to create voice notes directory: {e}");
let _ = std::fs::remove_file(&path);
if !drawing_filename.is_empty() {
let _ = std::fs::remove_file(state.config.data_dir.join("drawings").join(&drawing_filename));
}
return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
} }
if let Err(e) = entries::write_atomic(&vn_dir.join(&vn_name), bytes) { if let Err(e) = std::fs::write(vn_dir.join(&vn_name), bytes) {
tracing::error!("failed to write voice note: {e}"); tracing::error!("failed to write voice note: {e}");
let _ = std::fs::remove_file(&path);
if !drawing_filename.is_empty() {
let _ = std::fs::remove_file(state.config.data_dir.join("drawings").join(&drawing_filename));
}
return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
} }
vn_name vn_name
@ -343,22 +314,22 @@ async fn submit(
name, name,
date, date,
website, website,
drawing: drawing_filename.clone(), drawing: drawing_filename,
voice_note: voice_note_filename.clone(), voice_note: voice_note_filename,
status: Status::Pending, status: Status::Pending,
}, },
body: message, body: message,
}; };
if let Err(e) = entries::write_atomic(&path, entry.to_file_contents().as_bytes()) { // Write to disk
let entries_dir = state.config.data_dir.join("entries");
if let Err(e) = std::fs::create_dir_all(&entries_dir) {
tracing::error!("failed to create entries directory: {e}");
return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
}
let path = entries_dir.join(&filename);
if let Err(e) = std::fs::write(&path, entry.to_file_contents()) {
tracing::error!("failed to write entry: {e}"); tracing::error!("failed to write entry: {e}");
let _ = std::fs::remove_file(&path);
if !drawing_filename.is_empty() {
let _ = std::fs::remove_file(state.config.data_dir.join("drawings").join(&drawing_filename));
}
if !voice_note_filename.is_empty() {
let _ = std::fs::remove_file(state.config.data_dir.join("voice_notes").join(&voice_note_filename));
}
return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); return Html(render_error_page(&state.config, "Something went wrong. Please try again."));
} }