diff --git a/Cargo.lock b/Cargo.lock index 991bf0a..5cfb35c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,7 +470,7 @@ dependencies = [ [[package]] name = "guestbook" -version = "0.2.8" +version = "0.2.7" dependencies = [ "axum", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 733347b..f0efa95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "guestbook" -version = "0.2.8" +version = "0.2.7" 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." license = "MIT" diff --git a/src/entries.rs b/src/entries.rs index 9a0993e..43a9b6c 100644 --- a/src/entries.rs +++ b/src/entries.rs @@ -1,32 +1,6 @@ -use rand::Rng; use serde::{Deserialize, Serialize}; -use std::io::Write; 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)] #[serde(rename_all = "lowercase")] pub enum Status { @@ -175,16 +149,17 @@ pub fn append_reply(dir: &Path, id: &str, reply: &str) -> Result entry.body.push_str("\n\n"); entry.body.push_str("ed); 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()) } #[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 { let mut entry = find_entry(dir, short_id)?; entry.meta.status = status; 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()) } diff --git a/src/web.rs b/src/web.rs index 163fdd2..307424f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -49,20 +49,13 @@ pub fn router(state: Arc) -> Router { .with_state(state) } -fn generate_id(entries_dir: &std::path::Path) -> std::io::Result { +fn generate_id(entries_dir: &std::path::Path) -> String { const CHARS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; let mut rng = rand::rng(); loop { let id: String = (0..4).map(|_| CHARS[rng.random_range(0..36)] as char).collect(); - let path = entries_dir.join(format!("{id}.txt")); - match std::fs::OpenOptions::new() - .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), + if !entries_dir.join(format!("{id}.txt")).exists() { + return id; } } } @@ -279,33 +272,19 @@ async fn submit( let now = chrono::Utc::now(); let date = now.format("%Y-%m-%dT%H:%M:%S").to_string(); - - 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 id = generate_id(&state.config.data_dir.join("entries")); 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_name = format!("{id}.png"); let drawings_dir = state.config.data_dir.join("drawings"); if let Err(e) = std::fs::create_dir_all(&drawings_dir) { 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.")); } - 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}"); - let _ = std::fs::remove_file(&path); return Html(render_error_page(&state.config, "Something went wrong. Please try again.")); } drawing_name @@ -318,18 +297,10 @@ async fn submit( let vn_dir = state.config.data_dir.join("voice_notes"); if let Err(e) = std::fs::create_dir_all(&vn_dir) { 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.")); } - 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}"); - 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.")); } vn_name @@ -343,22 +314,22 @@ async fn submit( name, date, website, - drawing: drawing_filename.clone(), - voice_note: voice_note_filename.clone(), + drawing: drawing_filename, + voice_note: voice_note_filename, status: Status::Pending, }, 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}"); - 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.")); }