Add delete marker support to paths

Introduce DeleteMarker struct and associated functions for managing
soft-delete markers within case directories. This enables tracking
deleted cases and supports undo functionality.
feat: Add delete marker support to paths

Introduces a `.deleted` file to mark cases as soft-deleted. The marker
contains a `batch` UUID for grouping deletions and a `deleted_at`
timestamp for ordering.

New functions `is_deleted`, `read_delete_marker`, and
`write_delete_marker` are added to manage this marker. The `locate_case`
function is updated to also consider soft-deleted cases as not found.
This commit is contained in:
2026-04-15 22:41:47 +02:00
parent 11eb645f3f
commit 36b3e5f6c1
4 changed files with 51 additions and 6 deletions
+1
View File
@@ -2045,6 +2045,7 @@ checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"serde_core",
"wasm-bindgen",
]
+1 -1
View File
@@ -14,7 +14,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
dotenvy = "0.15"
uuid = { version = "1", features = ["v4"] }
uuid = { version = "1", features = ["v4", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
+41
View File
@@ -1,5 +1,8 @@
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Absolute on-disk location of a case directory.
///
/// Single source of truth for the layout `<data_path>/<slug>/<case_id>/`.
@@ -8,3 +11,41 @@ use std::path::{Path, PathBuf};
pub fn case_dir(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
data_path.join(slug).join(case_id)
}
/// Filename of the soft-delete marker inside a case directory.
pub const DELETE_MARKER: &str = ".deleted";
/// Soft-delete marker payload. One file per deleted case; all cases removed
/// in the same user click share the same `batch` UUID. `deleted_at` is an
/// RFC3339 timestamp — used to find the most recent batch for "Undo last
/// delete".
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMarker {
pub batch: Uuid,
pub deleted_at: String,
}
/// True iff the case directory contains a `.deleted` marker file.
pub async fn is_deleted(case_dir: &Path) -> bool {
tokio::fs::try_exists(case_dir.join(DELETE_MARKER))
.await
.unwrap_or(false)
}
/// Read and parse the `.deleted` marker. Returns `None` if absent or malformed
/// (treated identically — a malformed marker still means "deleted" via
/// `is_deleted`, but cannot participate in batch-undo).
pub async fn read_delete_marker(case_dir: &Path) -> Option<DeleteMarker> {
let bytes = tokio::fs::read(case_dir.join(DELETE_MARKER)).await.ok()?;
serde_json::from_slice(&bytes).ok()
}
/// Write the `.deleted` marker as JSON. Overwrites any existing marker.
pub async fn write_delete_marker(
case_dir: &Path,
marker: &DeleteMarker,
) -> std::io::Result<()> {
let bytes = serde_json::to_vec(marker)
.map_err(|e| std::io::Error::other(format!("serialize delete marker: {e}")))?;
tokio::fs::write(case_dir.join(DELETE_MARKER), bytes).await
}
+8 -5
View File
@@ -166,14 +166,17 @@ pub async fn handle_case_detail(
}
/// Locate a case directory under `<user_root>/<case_id>/`. Returns `None`
/// if the directory does not exist (treated as a 404 / IDOR probe by callers).
/// if the directory does not exist OR is soft-deleted (`.deleted` marker
/// present). Both are treated as 404 / IDOR probe by callers.
pub(crate) async fn locate_case(user_root: &Path, case_id: &str) -> Option<std::path::PathBuf> {
let p = user_root.join(case_id);
if tokio::fs::try_exists(&p).await.unwrap_or(false) {
Some(p)
} else {
None
if !tokio::fs::try_exists(&p).await.unwrap_or(false) {
return None;
}
if crate::paths::is_deleted(&p).await {
return None;
}
Some(p)
}
/// Scan all of the given user's cases. Returned vec is sorted by most