bug: delete_folder does not remove children or their files from disk #12
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_voice#12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
delete_folderincrates/hero_voice/src/voice/server/rpc.rs:224-228only deletes the folder database record. It does not handle any of its children.What is missing
parent_pathpointing to the deleted folder are not deletedCurrent behavior
After deleting a folder, all its children become orphaned — still in the database and still consuming disk space, but unreachable in the tree UI.
Expected behavior
Deleting a folder should recursively delete all child topics and sub-folders, including their audio and transform files on disk, similar to how
delete_topic(lines 198-222) already handles file cleanup for a single topic.File
crates/hero_voice/src/voice/server/rpc.rs:224-228Implementation Spec: Recursive folder delete with full filesystem cleanup
Objective
Make
VoiceService::delete_folderdelete the target folder together with every descendant folder and topic, including the audio and transform files those topics hold on disk, so the filesystem and database stay consistent after a folder is removed.Requirements
parent_pathas the deleted folder, but under a different branch) remain untouched.folder_sidstill returnsNotFound, matching current behaviour.delete_topicis today (fs::remove_dir_all(...).ok()), but database deletions propagate errors: if any descendant's DB delete fails, the whole operation returnsInternalwith the underlying message.Files to Modify / Create
crates/hero_voice/src/voice/server/rpc.rs— replace the one-linedelete_folderbody with a recursive implementation and add one#[test]. This is the only source file touched.No new files. No changes to generated code, schema, UI, or SDK.
Background / Clarifications
Two points in the issue needed confirmation before writing the traversal. Findings:
parent_path? — It is the slash-joined path of ancestor folder names, not a sid and not a single name.app.jsbuilds it withfolder.parent_path ? ${folder.parent_path}/${folder.name} : folder.name(seecrates/hero_voice_ui/static/app.jslines 108 and 143), andcreate_topic/create_folder(rpc.rs:108-162) store whatever the caller passes verbatim. A folder created withname = "projects"underparent_path = "work"hasparent_path = "work"and the full path other objects refer to it by is"work/projects"."".app.js:159callsbuildLevel(''), and the UI passesparent_path: parentPath || ''. Leading slashes are never used. The traversal must therefore compute a folder's full path as:""+ folder name at root, i.e. justfolder.name, whenfolder.parent_path == "".format!("{}/{}", folder.parent_path, folder.name)otherwise.Matching this convention is the key to finding children correctly: a folder F is a child of P when
F.parent_path == full_path(P); a topic T is a child of P under the same condition.Implementation Plan
Step 1 — Add a small path helper
File:
crates/hero_voice/src/voice/server/rpc.rs(helpers section, near the other free functions around lines 18-79).Add one private helper to keep the path convention in exactly one place:
Rationale: used both by
delete_folderto compute the target path once and by tests. Doc-comment the convention so future changes stay aligned withapp.js.Dependencies: none.
Step 2 — Replace
delete_folderwith a recursive, cascading implementationFile:
crates/hero_voice/src/voice/server/rpc.rs, currently lines 238-242.Replace the body. Pseudocode:
Where
delete_folder_childrenis a small private helper onVoiceServiceHandlerthat descends depth-first, deletes descendant topics viadelete_topic, then deletes descendant folder records viafolder_delete. AMAX_FOLDER_DEPTH = 64budget guards against cycles.Design notes:
delete_topicso audio/transforms cleanup lives in one place.Internal(matching file style); fs cleanup stays best-effort via.ok()insidedelete_topic.Dependencies: Step 1.
Step 3 — Add one
#[test]exercising all four casesFile: same file, inside
#[cfg(test)] mod tests { ... }, alongsidedelete_audio_branches. Single-test-per-file pattern: one#[test]fndelete_folder_cascadeswith the sameHERO_VOICE_DATA-based harness.Scenarios:
Dependencies: Steps 1 and 2.
Step 4 — Local verification
Run
cargo test -p hero_voice --liband confirm no regressions.Acceptance Criteria
VoiceService::delete_folderremoves the folder's DB record.VoiceService::delete_folderremoves every descendant topic's DB record, audio directory, and transforms directory.VoiceService::delete_folderremoves every descendant sub-folder's DB record, at any depth.VoiceService::delete_folderleaves sibling folders and their descendants untouched.VoiceService::delete_folderstill returnsNotFoundwhen called with an unknown sid.MAX_FOLDER_DEPTH = 64) that returnsInternalrather than panicking on cycle/overflow.audio_dir_for/transforms_dir_forcleanup logic — both paths are handled via the existingdelete_topicmethod.#[test] fn delete_folder_cascadesinvoice/server/rpc.rscovers empty folder, single-topic folder, nested sub-tree, and sibling-isolation scenarios, using theHERO_VOICE_DATA-based harness already established in the file.cargo test -p hero_voice --libpasses.crates/hero_voice_ui,crates/hero_voice_sdk, the OSchema generated files, or any other crate.Notes
delete_topicswallowsfs::remove_dir_allerrors with.ok(). The cascading delete preserves that behaviour by reusingdelete_topic. Changing it to surface fs errors is a broader policy decision and out of scope.delete_folder_childrenscansfolder_list()andtopic_list()at each recursion level — O(depth × (F + T)). Fine for expected volumes; an index could be a follow-up.rename_folder/move_folder. Neither method updates descendants'parent_path, which is a separate, pre-existing bug. Flagging here so future work doesn't conflict with this traversal predicate. Out of scope.folder_trigger_delete_preis a no-op returningtrue; no change needed.topic_deletefails mid-cascade, some descendants are already gone. Matches the file's non-transactional style; callable retries onNotFoundare safe.Test Results
Command:
cargo test -p hero_voice --features voiceThe new
voice::server::rpc::tests::delete_folder_cascadespassed, covering all four scenarios from the spec (empty folder, single-topic folder, nested sub-tree, sibling isolation). All pre-existing tests continue to pass.Implementation Summary
Changes
Single-file change:
crates/hero_voice/src/voice/server/rpc.rs.MAX_FOLDER_DEPTH = 64— depth-budget guard against cycles in the folder graph.folder_full_path(&Folder) -> String— centralises theparent_path + "/" + nameconvention used by the UI's tree builder.VoiceService::delete_folderto cascade:NotFoundon unknown sid).delete_topicso audio-dir and transforms-dir cleanup stays in one place.delete_folder_children(snapshot-then-iterate to avoid iterator invalidation; budget-checked).VoiceServiceHandler::delete_folder_childrenin a siblingimplblock.#[cfg(test)] mod testswith one#[test] fn delete_folder_cascadescovering:Scope preserved
fs::remove_dir_all(...).ok()insidedelete_topic), matching the pre-existing policy. DB errors from descendant deletes propagate asInternal.Acceptance criteria — status
delete_folderremoves the folder's DB record.delete_folderremoves every descendant topic's DB record, audio directory, and transforms directory.delete_folderremoves every descendant sub-folder's DB record at any depth.delete_folderleaves sibling folders and their descendants untouched.delete_folderstill returnsNotFoundwhen called with an unknown sid.Internalrather than panicking on cycle/overflow.audio_dir_for/transforms_dir_forcleanup logic.delete_folder_cascadestest covers all four scenarios.cargo test -p hero_voice --features voicepasses (14 total, 14 passed, 0 failed).crates/hero_voice_ui,crates/hero_voice_sdk, generated files, or any other crate.Test Results
14 passed, 0 failed. Full log in the preceding comment.
Known limitations / follow-ups (unchanged, noted in spec)
rename_folderandmove_folderstill leave descendants'parent_pathpointing at the old path. Separate pre-existing bug; not addressed here.delete_folder_childrenis O(depth x (folders + topics)). Fine for current data volumes; an index onparent_pathwould be the natural optimisation if the dataset grows.Scope expansion
While wrapping up, the
rename_folder/move_foldercascade bug noted in the "Notes" section of the spec (and originally deferred) was folded into the same PR. Same file, same root-cause pattern (mutations to a folder's full path don't propagate to descendants), same test harness.Additional changes to
crates/hero_voice/src/voice/server/rpc.rsrewrite_path(path, old_prefix, new_prefix) -> Option<String>— returns a rewrittenparent_pathiff the input starts withold_prefixas a path segment (exact match or followed by/). Avoids the classicstarts_withfootgun where"work"would wrongly match"work_backup".VoiceServiceHandler::propagate_path_change(old_prefix, new_prefix)— walks every folder and topic, appliesrewrite_path, persists the updated records. DB errors propagate asInternal.rename_foldernow: captures old full path, renames, captures new full path, persists the renamed folder, then callspropagate_path_changeso every descendant'sparent_pathfollows.move_foldernow: captures old full path, rejects moving a folder into itself or any of its descendants (InvalidInput), updatesparent_path, persists, then cascades.rename_and_move_folder_cascadescovering:work_backupvswork) stays untouched.InvalidInputand the tree state stays unchanged.Mutexbefore callingstd::env::set_current_dir, since cargo runs tests in parallel by default and process-wide CWD mutation would race otherwise.Final Test Results
Command:
cargo test -p hero_voice --features voicecargo clippy -p hero_voice --features voice --all-targets -- -D warningsalso passes cleanly.Acceptance criteria — final status
All items from the original spec pass (see previous summary comment). In addition:
rename_foldercascades the new name into every descendant'sparent_path.move_foldercascades the new parent path into every descendant'sparent_path.move_folderrejects self-and-descendant destinations, preventing tree cycles.Files changed
Still single-file:
crates/hero_voice/src/voice/server/rpc.rs. No UI, SDK, schema, or generated-code touches.Pull request opened: #16
The PR also folds in fixes for the
rename_folder/move_foldercascade bugs that were originally deferred as "separate pre-existing bug" in the spec.