Refactor: Simplify case directory structure

The distinction between `open/` and `done/` directories for cases has
been removed.
All cases for a user now reside directly under the user's directory
(e.g., `<data_path>/<slug>/<case_id>/`).

This simplifies path management and eliminates redundant directory
traversals.

Key changes include:
- Removed `open/` and `done/` subdirectories in path resolution.
- Introduced a `paths::case_dir` function as a single source of truth
  for case directory layout.
- Updated various modules (`recovery`, `auth`, `routes`, `transcribe`,
  `tests`) to use the new path structure.
- Adjusted templates to reflect the simplified case status
  representation.
This commit is contained in:
2026-04-15 22:15:13 +02:00
parent 70eed20909
commit 11eb645f3f
17 changed files with 190 additions and 324 deletions
+47 -54
View File
@@ -10,6 +10,7 @@ use tracing::warn;
use crate::config::Config;
use crate::error::AppError;
use crate::routes::user_web::any_document_exists;
pub(crate) struct RecordingView {
pub(crate) filename: String,
@@ -23,7 +24,7 @@ struct CaseView {
user_slug: String,
case_id: String,
case_id_short: String,
status: String,
has_document: bool,
most_recent: String,
oneliner: Option<String>,
recordings: Vec<RecordingView>,
@@ -55,17 +56,10 @@ pub async fn handle_audio(
.map_err(|_| AppError::BadRequest("Invalid case_id (not a UUID)".into()))?;
validate_filename(&filename)?;
let base = &config.data_path;
let open_path = base.join(&user).join("open").join(&case_id).join(&filename);
let done_path = base.join(&user).join("done").join(&case_id).join(&filename);
let file_path = if tokio::fs::try_exists(&open_path).await.unwrap_or(false) {
open_path
} else if tokio::fs::try_exists(&done_path).await.unwrap_or(false) {
done_path
} else {
let file_path = crate::paths::case_dir(&config.data_path, &user, &case_id).join(&filename);
if !tokio::fs::try_exists(&file_path).await.unwrap_or(false) {
return Err(AppError::NotFound("Audio file not found".into()));
};
}
let bytes = tokio::fs::read(&file_path).await?;
@@ -116,52 +110,51 @@ async fn scan_cases(data_path: &Path) -> Vec<CaseView> {
continue;
}
for status in ["open", "done"] {
let status_dir = user_entry.path().join(status);
let mut case_entries = match tokio::fs::read_dir(&status_dir).await {
Ok(r) => r,
let mut case_entries = match tokio::fs::read_dir(user_entry.path()).await {
Ok(r) => r,
Err(_) => continue,
};
while let Ok(Some(case_entry)) = case_entries.next_entry().await {
let case_id = match case_entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
while let Ok(Some(case_entry)) = case_entries.next_entry().await {
let case_id = match case_entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
if uuid::Uuid::parse_str(&case_id).is_err() {
warn!(case_id = %case_id, "Skipping non-UUID directory");
continue;
}
if !case_entry.path().is_dir() {
continue;
}
let recordings = scan_recordings(&case_entry.path()).await;
if recordings.is_empty() {
continue;
}
let most_recent = recordings
.last()
.map(|r| r.filename.clone())
.unwrap_or_default();
let case_id_short = case_id.chars().take(8).collect();
let oneliner = tokio::fs::read_to_string(case_entry.path().join("oneliner.txt"))
.await
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
cases.push(CaseView {
user_slug: user_slug.clone(),
case_id,
case_id_short,
status: status.to_string(),
most_recent,
oneliner,
recordings,
});
if uuid::Uuid::parse_str(&case_id).is_err() {
warn!(case_id = %case_id, "Skipping non-UUID directory");
continue;
}
if !case_entry.path().is_dir() {
continue;
}
let case_path = case_entry.path();
let recordings = scan_recordings(&case_path).await;
if recordings.is_empty() {
continue;
}
let most_recent = recordings
.last()
.map(|r| r.filename.clone())
.unwrap_or_default();
let case_id_short = case_id.chars().take(8).collect();
let oneliner = tokio::fs::read_to_string(case_path.join("oneliner.txt"))
.await
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let has_document = any_document_exists(&case_path).await;
cases.push(CaseView {
user_slug: user_slug.clone(),
case_id,
case_id_short,
has_document,
most_recent,
oneliner,
recordings,
});
}
}