refactor: rename soft-delete marker from .deleted to .closed

Internal rename: DELETE_MARKER -> CLOSE_MARKER, DeleteMarker ->
CloseMarker, is_deleted -> is_closed. The batch UUID is dropped;
undo now groups cases by their exact closed_at timestamp, which
bulk-close writes identically across its selection while per-case
close writes a unique stamp.

Behavior unchanged. No migration (project is pre-production);
legacy .deleted files in tmpdata were removed manually.
This commit is contained in:
2026-04-21 10:36:55 +02:00
parent a8389a89db
commit 9410d6daaa
13 changed files with 131 additions and 145 deletions
+22 -22
View File
@@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use doctate_common::oneliners::{ONELINER_FILENAME, OnelinerState};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
/// Absolute on-disk location of a case directory.
///
@@ -14,39 +13,40 @@ 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";
/// Filename of the close marker inside a case directory. A case with
/// this marker is "closed": hidden from the default listing, reversible
/// via a new upload or the explicit reopen action, and eligible for
/// lazy auto-purge after `auto_delete_days`.
pub const CLOSE_MARKER: &str = ".closed";
/// 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".
/// Close-marker payload. `closed_at` is an RFC3339 timestamp and the
/// authoritative input for purge eligibility (`now - closed_at > auto_delete_days`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteMarker {
pub batch: Uuid,
pub deleted_at: String,
pub struct CloseMarker {
pub closed_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))
/// True iff the case directory contains a `.closed` marker file.
pub async fn is_closed(case_dir: &Path) -> bool {
tokio::fs::try_exists(case_dir.join(CLOSE_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()?;
/// Read and parse the `.closed` marker. Returns `None` if absent or
/// malformed (treated identically — a malformed marker still means
/// "closed" via `is_closed`, but cannot participate in timestamp-based
/// grouping or purge checks).
pub async fn read_close_marker(case_dir: &Path) -> Option<CloseMarker> {
let bytes = tokio::fs::read(case_dir.join(CLOSE_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<()> {
/// Write the `.closed` marker as JSON. Overwrites any existing marker.
pub async fn write_close_marker(case_dir: &Path, marker: &CloseMarker) -> 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
.map_err(|e| std::io::Error::other(format!("serialize close marker: {e}")))?;
tokio::fs::write(case_dir.join(CLOSE_MARKER), bytes).await
}
/// Read the persisted oneliner state for a case.