feat: Add endpoint to delete recordings
This commit introduces a new API endpoint for deleting recordings. The endpoint handles the removal of the audio file and its associated sidecar files (transcript, duration). It also invalidates derived artifacts like oneliner.json, document.md, and analysis_input.json. Additionally, it includes checks for path traversal, correct file extensions, and case ownership to ensure security and data integrity. A new `RecordingDeleted` event is also added to the `CaseEventKind` enum.
This commit is contained in:
@@ -19,6 +19,7 @@ use crate::case_id::{CaseId, CaseIdPath};
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use crate::events::EventSender;
|
||||
use crate::gazetteer::Gazetteer;
|
||||
use crate::paths;
|
||||
use crate::routes::case_actions::read_document;
|
||||
use crate::routes::web::{RecordingView, scan_recordings};
|
||||
@@ -251,14 +252,39 @@ pub(crate) async fn any_document_exists(case_dir: &Path) -> bool {
|
||||
/// Self-heal: if a worker is idle but orphans exist on disk for this user,
|
||||
/// re-enqueue them. Page-load is the trigger; no cron, no periodic task.
|
||||
/// Runs for both pipelines so a page-load on either view cleans both.
|
||||
///
|
||||
/// Also opportunistically regenerates missing oneliners for the user — a
|
||||
/// recording-delete leaves `oneliner.json` gone, and without this heal
|
||||
/// call the stale state would only recover on server restart.
|
||||
impl PipelineState {
|
||||
pub async fn heal_orphans_if_idle(&self, user_root: &Path, slug: &str) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn heal_orphans_if_idle(
|
||||
&self,
|
||||
user_root: &Path,
|
||||
slug: &str,
|
||||
http_client: &reqwest::Client,
|
||||
config: &Config,
|
||||
vocab: &Gazetteer,
|
||||
events_tx: &EventSender,
|
||||
) {
|
||||
if !self.analyze_busy.0.load(Ordering::Acquire) {
|
||||
analyze_recovery::enqueue_pending_for_user(user_root, &self.analyze_tx).await;
|
||||
}
|
||||
if !self.transcribe_busy.0.load(Ordering::Acquire) {
|
||||
transcribe_recovery::enqueue_pending_for_user(user_root, slug, &self.transcribe_tx)
|
||||
.await;
|
||||
// Oneliner self-heal: if the transcribe worker is busy, it will
|
||||
// write a fresh oneliner at batch-end anyway — skip to avoid a
|
||||
// redundant LLM call.
|
||||
transcribe_recovery::regenerate_missing_oneliners_for_user(
|
||||
user_root,
|
||||
slug,
|
||||
http_client,
|
||||
config,
|
||||
vocab,
|
||||
events_tx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,9 +294,20 @@ pub async fn handle_my_cases(
|
||||
State(config): State<Arc<Config>>,
|
||||
State(pipeline): State<PipelineState>,
|
||||
State(events_tx): State<EventSender>,
|
||||
State(http_client): State<reqwest::Client>,
|
||||
State(vocab): State<Arc<Gazetteer>>,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
let user_root = config.data_path.join(&user.slug);
|
||||
pipeline.heal_orphans_if_idle(&user_root, &user.slug).await;
|
||||
pipeline
|
||||
.heal_orphans_if_idle(
|
||||
&user_root,
|
||||
&user.slug,
|
||||
&http_client,
|
||||
&config,
|
||||
&vocab,
|
||||
&events_tx,
|
||||
)
|
||||
.await;
|
||||
// Auto-analysis: hand every eligible case to the worker before we
|
||||
// render. The common case is "nothing to do" and costs a handful of
|
||||
// stat-calls per case; actual enqueues happen only when pre-conditions
|
||||
@@ -310,10 +347,21 @@ pub async fn handle_case_page(
|
||||
State(config): State<Arc<Config>>,
|
||||
State(pipeline): State<PipelineState>,
|
||||
State(events_tx): State<EventSender>,
|
||||
State(http_client): State<reqwest::Client>,
|
||||
State(vocab): State<Arc<Gazetteer>>,
|
||||
CaseIdPath(case_id): CaseIdPath,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
let user_root = config.data_path.join(&user.slug);
|
||||
pipeline.heal_orphans_if_idle(&user_root, &user.slug).await;
|
||||
pipeline
|
||||
.heal_orphans_if_idle(
|
||||
&user_root,
|
||||
&user.slug,
|
||||
&http_client,
|
||||
&config,
|
||||
&vocab,
|
||||
&events_tx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let case_dir = locate_case_or_404(
|
||||
&user_root,
|
||||
@@ -381,10 +429,22 @@ pub async fn handle_case_recordings(
|
||||
user: AuthenticatedWebUser,
|
||||
State(config): State<Arc<Config>>,
|
||||
State(pipeline): State<PipelineState>,
|
||||
State(events_tx): State<EventSender>,
|
||||
State(http_client): State<reqwest::Client>,
|
||||
State(vocab): State<Arc<Gazetteer>>,
|
||||
CaseIdPath(case_id): CaseIdPath,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
let user_root = config.data_path.join(&user.slug);
|
||||
pipeline.heal_orphans_if_idle(&user_root, &user.slug).await;
|
||||
pipeline
|
||||
.heal_orphans_if_idle(
|
||||
&user_root,
|
||||
&user.slug,
|
||||
&http_client,
|
||||
&config,
|
||||
&vocab,
|
||||
&events_tx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let case_dir = locate_case_or_404(
|
||||
&user_root,
|
||||
|
||||
Reference in New Issue
Block a user