max / audiofiles
13 files changed,
+113 insertions,
-80 deletions
| @@ -521,16 +521,19 @@ impl Backend for DirectBackend { | |||
| 521 | 521 | Ok(vfs::list_vfs(&db)?) | |
| 522 | 522 | } | |
| 523 | 523 | ||
| 524 | + | #[instrument(skip(self))] | |
| 524 | 525 | fn create_vfs(&self, name: &str) -> BackendResult<VfsId> { | |
| 525 | 526 | let db = self.db.lock(); | |
| 526 | 527 | Ok(vfs::create_vfs(&db, name)?) | |
| 527 | 528 | } | |
| 528 | 529 | ||
| 530 | + | #[instrument(skip(self))] | |
| 529 | 531 | fn rename_vfs(&self, id: VfsId, new_name: &str) -> BackendResult<()> { | |
| 530 | 532 | let db = self.db.lock(); | |
| 531 | 533 | Ok(vfs::rename_vfs(&db, id, new_name)?) | |
| 532 | 534 | } | |
| 533 | 535 | ||
| 536 | + | #[instrument(skip(self))] | |
| 534 | 537 | fn delete_vfs(&self, id: VfsId) -> BackendResult<()> { | |
| 535 | 538 | let db = self.db.lock(); | |
| 536 | 539 | Ok(vfs::delete_vfs(&db, id)?) | |
| @@ -554,6 +557,7 @@ impl Backend for DirectBackend { | |||
| 554 | 557 | Ok(vfs::list_children(&db, vfs_id, parent_id)?) | |
| 555 | 558 | } | |
| 556 | 559 | ||
| 560 | + | #[instrument(skip(self))] | |
| 557 | 561 | fn create_directory( | |
| 558 | 562 | &self, | |
| 559 | 563 | vfs_id: VfsId, | |
| @@ -564,6 +568,7 @@ impl Backend for DirectBackend { | |||
| 564 | 568 | Ok(vfs::create_directory(&db, vfs_id, parent_id, name)?) | |
| 565 | 569 | } | |
| 566 | 570 | ||
| 571 | + | #[instrument(skip(self))] | |
| 567 | 572 | fn create_sample_link( | |
| 568 | 573 | &self, | |
| 569 | 574 | vfs_id: VfsId, | |
| @@ -585,21 +590,25 @@ impl Backend for DirectBackend { | |||
| 585 | 590 | Ok(vfs::get_breadcrumb(&db, node_id)?) | |
| 586 | 591 | } | |
| 587 | 592 | ||
| 593 | + | #[instrument(skip(self))] | |
| 588 | 594 | fn rename_node(&self, id: NodeId, new_name: &str) -> BackendResult<()> { | |
| 589 | 595 | let db = self.db.lock(); | |
| 590 | 596 | Ok(vfs::rename_node(&db, id, new_name)?) | |
| 591 | 597 | } | |
| 592 | 598 | ||
| 599 | + | #[instrument(skip(self))] | |
| 593 | 600 | fn move_node(&self, id: NodeId, new_parent_id: Option<NodeId>) -> BackendResult<()> { | |
| 594 | 601 | let db = self.db.lock(); | |
| 595 | 602 | Ok(vfs::move_node(&db, id, new_parent_id)?) | |
| 596 | 603 | } | |
| 597 | 604 | ||
| 605 | + | #[instrument(skip(self))] | |
| 598 | 606 | fn delete_node(&self, id: NodeId) -> BackendResult<()> { | |
| 599 | 607 | let db = self.db.lock(); | |
| 600 | 608 | Ok(vfs::delete_node(&db, id)?) | |
| 601 | 609 | } | |
| 602 | 610 | ||
| 611 | + | #[instrument(skip_all)] | |
| 603 | 612 | fn restore_node(&self, node: &VfsNode) -> BackendResult<()> { | |
| 604 | 613 | let db = self.db.lock(); | |
| 605 | 614 | Ok(vfs::restore_node(&db, node)?) | |
| @@ -626,11 +635,13 @@ impl Backend for DirectBackend { | |||
| 626 | 635 | ||
| 627 | 636 | // --- Tags --- | |
| 628 | 637 | ||
| 638 | + | #[instrument(skip(self))] | |
| 629 | 639 | fn add_tag(&self, hash: &str, tag: &str) -> BackendResult<()> { | |
| 630 | 640 | let db = self.db.lock(); | |
| 631 | 641 | Ok(tags::add_tag(&db, hash, tag)?) | |
| 632 | 642 | } | |
| 633 | 643 | ||
| 644 | + | #[instrument(skip(self))] | |
| 634 | 645 | fn remove_tag(&self, hash: &str, tag: &str) -> BackendResult<()> { | |
| 635 | 646 | let db = self.db.lock(); | |
| 636 | 647 | Ok(tags::remove_tag(&db, hash, tag)?) | |
| @@ -646,11 +657,13 @@ impl Backend for DirectBackend { | |||
| 646 | 657 | Ok(tags::list_all_tags(&db)?) | |
| 647 | 658 | } | |
| 648 | 659 | ||
| 660 | + | #[instrument(skip(self, hashes))] | |
| 649 | 661 | fn bulk_add_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize> { | |
| 650 | 662 | let db = self.db.lock(); | |
| 651 | 663 | Ok(tags::bulk_add_tag(&db, hashes, tag)?) | |
| 652 | 664 | } | |
| 653 | 665 | ||
| 666 | + | #[instrument(skip(self, hashes))] | |
| 654 | 667 | fn bulk_remove_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize> { | |
| 655 | 668 | let db = self.db.lock(); | |
| 656 | 669 | Ok(tags::bulk_remove_tag(&db, hashes, tag)?) | |
| @@ -695,6 +708,7 @@ impl Backend for DirectBackend { | |||
| 695 | 708 | Ok(collections::list_collections(&db)?) | |
| 696 | 709 | } | |
| 697 | 710 | ||
| 711 | + | #[instrument(skip(self))] | |
| 698 | 712 | fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId> { | |
| 699 | 713 | let db = self.db.lock(); | |
| 700 | 714 | Ok(collections::create_collection(&db, name, description)?) | |
| @@ -710,6 +724,7 @@ impl Backend for DirectBackend { | |||
| 710 | 724 | Ok(collections::rename_collection(&db, id, new_name)?) | |
| 711 | 725 | } | |
| 712 | 726 | ||
| 727 | + | #[instrument(skip(self))] | |
| 713 | 728 | fn delete_collection(&self, id: CollectionId) -> BackendResult<()> { | |
| 714 | 729 | let db = self.db.lock(); | |
| 715 | 730 | Ok(collections::delete_collection(&db, id)?) | |
| @@ -937,6 +952,7 @@ impl Backend for DirectBackend { | |||
| 937 | 952 | Ok(audiofiles_core::analysis::exemplar::get_policy(&db, tag)?) | |
| 938 | 953 | } | |
| 939 | 954 | ||
| 955 | + | #[instrument(skip(self))] | |
| 940 | 956 | fn set_tag_policy(&self, tag: &str, review: f64, auto: f64) -> BackendResult<()> { | |
| 941 | 957 | let db = self.db.lock(); | |
| 942 | 958 | Ok(audiofiles_core::analysis::exemplar::set_policy(&db, tag, review, auto)?) | |
| @@ -1386,6 +1402,7 @@ impl Backend for DirectBackend { | |||
| 1386 | 1402 | Ok(result) | |
| 1387 | 1403 | } | |
| 1388 | 1404 | ||
| 1405 | + | #[instrument(skip(self, value))] | |
| 1389 | 1406 | fn set_config(&self, key: &str, value: &str) -> BackendResult<()> { | |
| 1390 | 1407 | let db = self.db.lock(); | |
| 1391 | 1408 | db.conn() |
| @@ -160,10 +160,10 @@ mod tests { | |||
| 160 | 160 | let handle = spawn_search_worker(db_path).unwrap(); | |
| 161 | 161 | // A query against an empty library must still produce an event (empty | |
| 162 | 162 | // results or a clean error), never a hang or a panic that kills the worker. | |
| 163 | - | handle.send(SearchCommand::FindSimilar { | |
| 163 | + | assert!(handle.send(SearchCommand::FindSimilar { | |
| 164 | 164 | hash: "deadbeef".to_string(), | |
| 165 | 165 | limit: 10, | |
| 166 | - | }); | |
| 166 | + | })); | |
| 167 | 167 | match recv_one(&handle) { | |
| 168 | 168 | SearchEvent::SimilarResults { source, hashes } => { | |
| 169 | 169 | assert_eq!(source, "deadbeef"); | |
| @@ -174,11 +174,11 @@ mod tests { | |||
| 174 | 174 | } | |
| 175 | 175 | ||
| 176 | 176 | // Invalidate + a second query still respond (worker survived). | |
| 177 | - | handle.send(SearchCommand::Invalidate); | |
| 178 | - | handle.send(SearchCommand::FindNearDuplicates { | |
| 177 | + | assert!(handle.send(SearchCommand::Invalidate)); | |
| 178 | + | assert!(handle.send(SearchCommand::FindNearDuplicates { | |
| 179 | 179 | hash: "deadbeef".to_string(), | |
| 180 | 180 | limit: 10, | |
| 181 | - | }); | |
| 181 | + | })); | |
| 182 | 182 | let _ = recv_one(&handle); | |
| 183 | 183 | drop(handle); // Drop joins the thread; a wedged worker would hang here. | |
| 184 | 184 | } |
| @@ -113,7 +113,7 @@ mod tests { | |||
| 113 | 113 | let _db = Database::open(&db_path).unwrap(); | |
| 114 | 114 | ||
| 115 | 115 | let handle = spawn_loose_files_worker(db_path).unwrap(); | |
| 116 | - | handle.send(LooseFilesCommand::CheckIntegrity); | |
| 116 | + | assert!(handle.send(LooseFilesCommand::CheckIntegrity)); | |
| 117 | 117 | ||
| 118 | 118 | let mut got = false; | |
| 119 | 119 | for _ in 0..50 { |
| @@ -200,39 +200,39 @@ impl BrowserState { | |||
| 200 | 200 | match op { | |
| 201 | 201 | UndoOp::BulkDelete { nodes, tags: tag_data } => { | |
| 202 | 202 | for node in &nodes { | |
| 203 | - | let _ = self.backend.restore_node(node); | |
| 203 | + | super::log_backend_err("restore_node (undo delete)", self.backend.restore_node(node)); | |
| 204 | 204 | } | |
| 205 | 205 | for (hash, sample_tags) in &tag_data { | |
| 206 | 206 | for tag in sample_tags { | |
| 207 | - | let _ = self.backend.add_tag(hash, tag); | |
| 207 | + | super::log_backend_err("add_tag (undo delete)", self.backend.add_tag(hash, tag)); | |
| 208 | 208 | } | |
| 209 | 209 | } | |
| 210 | 210 | self.status = "Undo: restored deleted items".to_string(); | |
| 211 | 211 | } | |
| 212 | 212 | UndoOp::BulkMove { moves } => { | |
| 213 | 213 | for (node_id, old_parent) in &moves { | |
| 214 | - | let _ = self.backend.move_node(*node_id, *old_parent); | |
| 214 | + | super::log_backend_err("move_node (undo move)", self.backend.move_node(*node_id, *old_parent)); | |
| 215 | 215 | } | |
| 216 | 216 | self.status = "Undo: moved items back".to_string(); | |
| 217 | 217 | } | |
| 218 | 218 | UndoOp::BulkRename { renames } => { | |
| 219 | 219 | for (node_id, old_name) in &renames { | |
| 220 | - | let _ = self.backend.rename_node(*node_id, old_name); | |
| 220 | + | super::log_backend_err("rename_node (undo rename)", self.backend.rename_node(*node_id, old_name)); | |
| 221 | 221 | } | |
| 222 | 222 | self.status = "Undo: restored original names".to_string(); | |
| 223 | 223 | } | |
| 224 | 224 | UndoOp::BulkTagAdd { tag, hashes } => { | |
| 225 | 225 | let refs: Vec<&str> = hashes.iter().map(|s| s.as_str()).collect(); | |
| 226 | - | let _ = self.backend.bulk_remove_tag(&refs, &tag); | |
| 226 | + | super::log_backend_err("bulk_remove_tag (undo tag add)", self.backend.bulk_remove_tag(&refs, &tag)); | |
| 227 | 227 | self.status = "Undo: removed tag".to_string(); | |
| 228 | 228 | } | |
| 229 | 229 | UndoOp::BulkTagRemove { tag, hashes } => { | |
| 230 | 230 | let refs: Vec<&str> = hashes.iter().map(|s| s.as_str()).collect(); | |
| 231 | - | let _ = self.backend.bulk_add_tag(&refs, &tag); | |
| 231 | + | super::log_backend_err("bulk_add_tag (undo tag remove)", self.backend.bulk_add_tag(&refs, &tag)); | |
| 232 | 232 | self.status = "Undo: re-added tag".to_string(); | |
| 233 | 233 | } | |
| 234 | 234 | UndoOp::TagRemove { hash, tag } => { | |
| 235 | - | let _ = self.backend.add_tag(&hash, &tag); | |
| 235 | + | super::log_backend_err("add_tag (undo tag remove)", self.backend.add_tag(&hash, &tag)); | |
| 236 | 236 | self.status = format!("Undo: restored tag \"{tag}\""); | |
| 237 | 237 | } | |
| 238 | 238 | } |
| @@ -175,10 +175,10 @@ impl BrowserState { | |||
| 175 | 175 | /// when off (default) the signal is left untouched and only reported. | |
| 176 | 176 | pub fn toggle_forge_auto_trim_overshoot(&mut self) { | |
| 177 | 177 | self.forge_auto_trim_overshoot = !self.forge_auto_trim_overshoot; | |
| 178 | - | let _ = self.backend.set_config( | |
| 178 | + | super::log_backend_err("set_config forge_auto_trim_overshoot", self.backend.set_config( | |
| 179 | 179 | crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY, | |
| 180 | 180 | if self.forge_auto_trim_overshoot { "true" } else { "false" }, | |
| 181 | - | ); | |
| 181 | + | )); | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | 184 | /// Batch trim leading/trailing silence across the current selection. Reuses |
| @@ -73,7 +73,7 @@ impl BrowserState { | |||
| 73 | 73 | self.status = format!("Failed to save analysis for {n} samples: {e}"); | |
| 74 | 74 | return; | |
| 75 | 75 | } | |
| 76 | - | let _ = self.backend.apply_rules_to_samples(&hashes); | |
| 76 | + | super::log_backend_err("apply_rules_to_samples", self.backend.apply_rules_to_samples(&hashes)); | |
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | 79 | /// Quick-import a single file or directory via drag-and-drop into the current folder. | |
| @@ -276,7 +276,7 @@ impl BrowserState { | |||
| 276 | 276 | }; | |
| 277 | 277 | self.status = format!("Analyzing {total} samples..."); | |
| 278 | 278 | ||
| 279 | - | let _ = self.backend.start_analysis(sample_hashes, config); | |
| 279 | + | super::log_backend_err("start_analysis (import)", self.backend.start_analysis(sample_hashes, config)); | |
| 280 | 280 | } | |
| 281 | 281 | ||
| 282 | 282 | /// Cancel the running analysis batch and land in the acknowledgement | |
| @@ -428,7 +428,7 @@ impl BrowserState { | |||
| 428 | 428 | } | |
| 429 | 429 | }; | |
| 430 | 430 | ||
| 431 | - | let _ = self.backend.start_import(&source, strategy_desc); | |
| 431 | + | super::log_backend_err("start_import", self.backend.start_import(&source, strategy_desc)); | |
| 432 | 432 | ||
| 433 | 433 | self.import_file_errors.clear(); | |
| 434 | 434 | self.analysis_errors.clear(); | |
| @@ -669,7 +669,7 @@ impl BrowserState { | |||
| 669 | 669 | // Auto-accept all suggestions in quick mode | |
| 670 | 670 | for item in &items { | |
| 671 | 671 | for s in &item.suggestions { | |
| 672 | - | let _ = self.backend.add_tag(&item.hash, &s.suggestion.tag); | |
| 672 | + | super::log_backend_err("add_tag (import suggestion)", self.backend.add_tag(&item.hash, &s.suggestion.tag)); | |
| 673 | 673 | } | |
| 674 | 674 | } | |
| 675 | 675 | self.quick_import = false; | |
| @@ -977,7 +977,7 @@ impl BrowserState { | |||
| 977 | 977 | continue; | |
| 978 | 978 | } | |
| 979 | 979 | for (hash, _ext) in &entry.folder.samples { | |
| 980 | - | let _ = self.backend.add_tag(hash, tag_str); | |
| 980 | + | super::log_backend_err("add_tag (batch)", self.backend.add_tag(hash, tag_str)); | |
| 981 | 981 | applied += 1; | |
| 982 | 982 | } | |
| 983 | 983 | } | |
| @@ -1026,7 +1026,7 @@ impl BrowserState { | |||
| 1026 | 1026 | for item in items { | |
| 1027 | 1027 | for sug in &item.suggestions { | |
| 1028 | 1028 | if sug.accepted { | |
| 1029 | - | let _ = self.backend.add_tag(&item.hash, &sug.suggestion.tag); | |
| 1029 | + | super::log_backend_err("add_tag (suggestion)", self.backend.add_tag(&item.hash, &sug.suggestion.tag)); | |
| 1030 | 1030 | applied += 1; | |
| 1031 | 1031 | } | |
| 1032 | 1032 | } | |
| @@ -1150,7 +1150,7 @@ impl BrowserState { | |||
| 1150 | 1150 | .unwrap_or_default() | |
| 1151 | 1151 | }; | |
| 1152 | 1152 | ||
| 1153 | - | let _ = self.backend.enrich_export_with_tags(&mut items); | |
| 1153 | + | super::log_backend_err("enrich_export_with_tags", self.backend.enrich_export_with_tags(&mut items)); | |
| 1154 | 1154 | ||
| 1155 | 1155 | if items.is_empty() { | |
| 1156 | 1156 | self.status = "No samples to export".to_string(); | |
| @@ -1192,7 +1192,7 @@ impl BrowserState { | |||
| 1192 | 1192 | // still useful in the UI for "files already written remain at <path>". | |
| 1193 | 1193 | self.last_export_destination = Some(config.destination.clone()); | |
| 1194 | 1194 | self.operation_progress = Some(crate::state::OperationProgress::new()); | |
| 1195 | - | let _ = self.backend.start_export(items, config); | |
| 1195 | + | super::log_backend_err("start_export", self.backend.start_export(items, config)); | |
| 1196 | 1196 | ||
| 1197 | 1197 | self.import_mode = ImportMode::Exporting { | |
| 1198 | 1198 | completed: 0, | |
| @@ -1471,7 +1471,7 @@ impl BrowserState { | |||
| 1471 | 1471 | super::EditResultMode::Replace => "replace", | |
| 1472 | 1472 | super::EditResultMode::Sibling => "sibling", | |
| 1473 | 1473 | }; | |
| 1474 | - | let _ = self.backend.set_config("edit_result_mode", mode_str); | |
| 1474 | + | super::log_backend_err("set_config edit_result_mode", self.backend.set_config("edit_result_mode", mode_str)); | |
| 1475 | 1475 | self.edit.result_mode = Some(mode); | |
| 1476 | 1476 | } | |
| 1477 | 1477 | ||
| @@ -1541,7 +1541,7 @@ impl BrowserState { | |||
| 1541 | 1541 | }; | |
| 1542 | 1542 | ||
| 1543 | 1543 | // 2. Record in edit_history | |
| 1544 | - | let _ = self.backend.record_edit_history(&source_hash, &new_hash, &operation); | |
| 1544 | + | super::log_backend_err("record_edit_history", self.backend.record_edit_history(&source_hash, &new_hash, &operation)); | |
| 1545 | 1545 | ||
| 1546 | 1546 | // 3. Update VFS based on mode | |
| 1547 | 1547 | let Some(vfs_id) = self.current_vfs_id() else { | |
| @@ -1568,8 +1568,8 @@ impl BrowserState { | |||
| 1568 | 1568 | let parent_id = node.node.parent_id; | |
| 1569 | 1569 | let name = node.node.name.clone(); | |
| 1570 | 1570 | replace_targets.push((parent_id, name.clone())); | |
| 1571 | - | let _ = self.backend.delete_node(node.node.id); | |
| 1572 | - | let _ = self.backend.create_sample_link(vfs_id, parent_id, &name, &new_hash); | |
| 1571 | + | super::log_backend_err("delete_node (edit VFS update)", self.backend.delete_node(node.node.id)); | |
| 1572 | + | super::log_backend_err("create_sample_link (edit replace)", self.backend.create_sample_link(vfs_id, parent_id, &name, &new_hash)); | |
| 1573 | 1573 | } | |
| 1574 | 1574 | } | |
| 1575 | 1575 | super::EditResultMode::Sibling => { | |
| @@ -1605,7 +1605,7 @@ impl BrowserState { | |||
| 1605 | 1605 | ||
| 1606 | 1606 | // 4. Trigger analysis on the new sample | |
| 1607 | 1607 | let hashes = vec![(new_hash, new_ext)]; | |
| 1608 | - | let _ = self.backend.start_analysis(hashes, AnalysisConfig::default()); | |
| 1608 | + | super::log_backend_err("start_analysis (post-edit)", self.backend.start_analysis(hashes, AnalysisConfig::default())); | |
| 1609 | 1609 | ||
| 1610 | 1610 | // 5. Refresh the file list | |
| 1611 | 1611 | self.refresh_contents(); | |
| @@ -1634,21 +1634,24 @@ impl BrowserState { | |||
| 1634 | 1634 | .find_nodes_by_hashes(entry.vfs_id, &result_hashes) | |
| 1635 | 1635 | { | |
| 1636 | 1636 | for node in &result_nodes { | |
| 1637 | - | let _ = self.backend.delete_node(node.node.id); | |
| 1637 | + | super::log_backend_err("delete_node (edit VFS update)", self.backend.delete_node(node.node.id)); | |
| 1638 | 1638 | } | |
| 1639 | 1639 | } | |
| 1640 | 1640 | for (parent_id, name) in &entry.replace_targets { | |
| 1641 | - | let _ = self.backend.create_sample_link( | |
| 1642 | - | entry.vfs_id, | |
| 1643 | - | *parent_id, | |
| 1644 | - | name, | |
| 1645 | - | &entry.source_hash, | |
| 1641 | + | super::log_backend_err( | |
| 1642 | + | "create_sample_link (edit undo)", | |
| 1643 | + | self.backend.create_sample_link( | |
| 1644 | + | entry.vfs_id, | |
| 1645 | + | *parent_id, | |
| 1646 | + | name, | |
| 1647 | + | &entry.source_hash, | |
| 1648 | + | ), | |
| 1646 | 1649 | ); | |
| 1647 | 1650 | } | |
| 1648 | 1651 | } | |
| 1649 | 1652 | super::EditResultMode::Sibling => { | |
| 1650 | 1653 | if let Some(id) = entry.sibling_node_id { | |
| 1651 | - | let _ = self.backend.delete_node(id); | |
| 1654 | + | super::log_backend_err("delete_node (edit undo sibling)", self.backend.delete_node(id)); | |
| 1652 | 1655 | } | |
| 1653 | 1656 | } | |
| 1654 | 1657 | } |
| @@ -142,14 +142,14 @@ impl BrowserState { | |||
| 142 | 142 | /// Dismiss the first-launch hint and persist the preference. | |
| 143 | 143 | pub fn dismiss_first_launch_hint(&mut self) { | |
| 144 | 144 | self.show_first_launch_hint = false; | |
| 145 | - | let _ = self.backend.set_config("hints_dismissed", "1"); | |
| 145 | + | super::log_backend_err("set_config hints_dismissed", self.backend.set_config("hints_dismissed", "1")); | |
| 146 | 146 | } | |
| 147 | 147 | ||
| 148 | 148 | /// Re-surface the welcome screen for a user who dismissed it. Persists the | |
| 149 | 149 | /// reset so the welcome renders again on next launch until re-dismissed. | |
| 150 | 150 | pub fn show_welcome(&mut self) { | |
| 151 | 151 | self.show_first_launch_hint = true; | |
| 152 | - | let _ = self.backend.set_config("hints_dismissed", "0"); | |
| 152 | + | super::log_backend_err("set_config hints_dismissed", self.backend.set_config("hints_dismissed", "0")); | |
| 153 | 153 | } | |
| 154 | 154 | ||
| 155 | 155 | /// Dismiss the sync-intro banner and persist the preference. Called both | |
| @@ -157,7 +157,7 @@ impl BrowserState { | |||
| 157 | 157 | /// clicks "Set up sync" — the banner should not re-appear after either. | |
| 158 | 158 | pub fn dismiss_sync_intro(&mut self) { | |
| 159 | 159 | self.show_sync_intro = false; | |
| 160 | - | let _ = self.backend.set_config("sync_intro_dismissed", "1"); | |
| 160 | + | super::log_backend_err("set_config sync_intro_dismissed", self.backend.set_config("sync_intro_dismissed", "1")); | |
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | 163 | /// Whether the user has work in progress that would be interrupted by a | |
| @@ -173,7 +173,7 @@ impl BrowserState { | |||
| 173 | 173 | /// Used from Settings / Help to bring back onboarding context after dismissal. | |
| 174 | 174 | pub fn reset_vfs_explanation(&mut self) { | |
| 175 | 175 | self.show_vfs_banner = true; | |
| 176 | - | let _ = self.backend.set_config("vfs_explained", "0"); | |
| 176 | + | super::log_backend_err("set_config vfs_explained", self.backend.set_config("vfs_explained", "0")); | |
| 177 | 177 | } | |
| 178 | 178 | ||
| 179 | 179 | /// Globally rename a tag: every sample that carries `old_tag` will instead | |
| @@ -355,7 +355,7 @@ impl BrowserState { | |||
| 355 | 355 | ||
| 356 | 356 | /// Save the current theme ID to the user_config table. | |
| 357 | 357 | pub fn save_theme_preference(&self) { | |
| 358 | - | let _ = self.backend.set_config("theme", &self.current_theme_id); | |
| 358 | + | super::log_backend_err("set_config theme", self.backend.set_config("theme", &self.current_theme_id)); | |
| 359 | 359 | } | |
| 360 | 360 | ||
| 361 | 361 | /// Reset column visibility, sort, and row density to defaults. The Settings | |
| @@ -368,7 +368,7 @@ impl BrowserState { | |||
| 368 | 368 | self.sort_column = SortColumn::Name; | |
| 369 | 369 | self.sort_direction = SortDirection::Ascending; | |
| 370 | 370 | self.save_column_config(); | |
| 371 | - | let _ = self.backend.set_config("row_height", "24"); | |
| 371 | + | super::log_backend_err("set_config row_height", self.backend.set_config("row_height", "24")); | |
| 372 | 372 | self.status = "Columns reset to defaults".to_string(); | |
| 373 | 373 | } | |
| 374 | 374 | ||
| @@ -395,7 +395,7 @@ impl BrowserState { | |||
| 395 | 395 | fn save_dismissed_suggestions(&self) { | |
| 396 | 396 | // unwrap is safe: HashMap<String, Vec<String>> serialises cleanly. | |
| 397 | 397 | let json = serde_json::to_string(&self.dismissed_suggestions).unwrap(); | |
| 398 | - | let _ = self.backend.set_config("suggestions.dismissed", &json); | |
| 398 | + | super::log_backend_err("set_config suggestions.dismissed", self.backend.set_config("suggestions.dismissed", &json)); | |
| 399 | 399 | } | |
| 400 | 400 | ||
| 401 | 401 | /// Mark a tag suggestion as rejected for the given classification so it | |
| @@ -453,7 +453,7 @@ impl BrowserState { | |||
| 453 | 453 | // unwrap is safe: ColumnConfig contains only primitive fields (bools, enums) | |
| 454 | 454 | // with derived Serialize impls, so serialisation cannot fail. | |
| 455 | 455 | let json = serde_json::to_string(&self.column_config).unwrap(); | |
| 456 | - | let _ = self.backend.set_config("column_config", &json); | |
| 456 | + | super::log_backend_err("set_config column_config", self.backend.set_config("column_config", &json)); | |
| 457 | 457 | } | |
| 458 | 458 | ||
| 459 | 459 | // --- VFS Mirror --- |
| @@ -25,6 +25,19 @@ use audiofiles_core::{CollectionId, NodeId, VfsId}; | |||
| 25 | 25 | pub use audiofiles_core::vfs::VfsNodeWithAnalysis; | |
| 26 | 26 | use parking_lot::Mutex; | |
| 27 | 27 | ||
| 28 | + | /// Log a backend operation failure instead of silently swallowing it. | |
| 29 | + | /// | |
| 30 | + | /// Used at call sites that previously wrote `let _ = self.backend.op(...)`: a | |
| 31 | + | /// failed preference write, tag application, or undo step should be visible in | |
| 32 | + | /// the log rather than lost. These are fire-and-observe (the UI has already | |
| 33 | + | /// moved on), so they warn rather than surface a banner — but they are no longer | |
| 34 | + | /// invisible. `op` is a short static label identifying the call site. | |
| 35 | + | pub(crate) fn log_backend_err<T>(op: &str, result: crate::backend::BackendResult<T>) { | |
| 36 | + | if let Err(e) = result { | |
| 37 | + | tracing::warn!(op, error = %e, "backend operation failed"); | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 28 | 41 | /// A cheap-to-clone handle to the current folder's contents. | |
| 29 | 42 | /// | |
| 30 | 43 | /// Wraps `Arc<Vec<VfsNodeWithAnalysis>>` so the per-frame `clone()` in the file |
| @@ -299,10 +299,10 @@ impl BrowserState { | |||
| 299 | 299 | self.similarity_source_name = None; | |
| 300 | 300 | // Persist the selection by VFS id (not index — indices shift when | |
| 301 | 301 | // vaults are added or removed) so it restores on next launch. | |
| 302 | - | let _ = self.backend.set_config( | |
| 302 | + | super::log_backend_err("set_config current_vfs_id", self.backend.set_config( | |
| 303 | 303 | "current_vfs_id", | |
| 304 | 304 | &self.vfs_list[self.current_vfs_idx].id.as_i64().to_string(), | |
| 305 | - | ); | |
| 305 | + | )); | |
| 306 | 306 | self.refresh_contents(); | |
| 307 | 307 | self.refresh_collections(); | |
| 308 | 308 | self.status = format!("Switched to: {}", self.vfs_list[self.current_vfs_idx].name); | |
| @@ -312,10 +312,10 @@ impl BrowserState { | |||
| 312 | 312 | /// Toggle the left sidebar and persist the choice across restarts. | |
| 313 | 313 | pub fn toggle_sidebar(&mut self) { | |
| 314 | 314 | self.sidebar_visible = !self.sidebar_visible; | |
| 315 | - | let _ = self.backend.set_config( | |
| 315 | + | super::log_backend_err("set_config sidebar_visible", self.backend.set_config( | |
| 316 | 316 | "sidebar_visible", | |
| 317 | 317 | if self.sidebar_visible { "1" } else { "0" }, | |
| 318 | - | ); | |
| 318 | + | )); | |
| 319 | 319 | } | |
| 320 | 320 | ||
| 321 | 321 | /// Toggle the right detail panel and persist the choice across restarts. | |
| @@ -328,18 +328,20 @@ impl BrowserState { | |||
| 328 | 328 | pub fn set_detail_visible(&mut self, visible: bool) { | |
| 329 | 329 | if self.detail_visible != visible { | |
| 330 | 330 | self.detail_visible = visible; | |
| 331 | - | let _ = self | |
| 332 | - | .backend | |
| 333 | - | .set_config("detail_visible", if visible { "1" } else { "0" }); | |
| 331 | + | super::log_backend_err( | |
| 332 | + | "set_config detail_visible", | |
| 333 | + | self.backend | |
| 334 | + | .set_config("detail_visible", if visible { "1" } else { "0" }), | |
| 335 | + | ); | |
| 334 | 336 | } | |
| 335 | 337 | } | |
| 336 | 338 | ||
| 337 | 339 | /// Toggle the filter panel and persist the choice across restarts. | |
| 338 | 340 | pub fn toggle_filter_panel(&mut self) { | |
| 339 | 341 | self.filter_panel_open = !self.filter_panel_open; | |
| 340 | - | let _ = self.backend.set_config( | |
| 342 | + | super::log_backend_err("set_config filter_panel_open", self.backend.set_config( | |
| 341 | 343 | "filter_panel_open", | |
| 342 | 344 | if self.filter_panel_open { "1" } else { "0" }, | |
| 343 | - | ); | |
| 345 | + | )); | |
| 344 | 346 | } | |
| 345 | 347 | } |
| @@ -94,7 +94,7 @@ impl BrowserState { | |||
| 94 | 94 | /// Toggle loop mode and persist the setting. | |
| 95 | 95 | pub fn toggle_loop(&mut self) { | |
| 96 | 96 | self.loop_enabled = !self.loop_enabled; | |
| 97 | - | let _ = self.backend.set_config("preview_loop", if self.loop_enabled { "1" } else { "0" }); | |
| 97 | + | super::log_backend_err("set_config preview_loop", self.backend.set_config("preview_loop", if self.loop_enabled { "1" } else { "0" })); | |
| 98 | 98 | // Sync to the live playback state | |
| 99 | 99 | self.shared.preview.lock().loop_enabled = self.loop_enabled; | |
| 100 | 100 | } | |
| @@ -102,7 +102,7 @@ impl BrowserState { | |||
| 102 | 102 | /// Toggle autoplay mode and persist the setting. | |
| 103 | 103 | pub fn toggle_autoplay(&mut self) { | |
| 104 | 104 | self.autoplay = !self.autoplay; | |
| 105 | - | let _ = self.backend.set_config("preview_autoplay", if self.autoplay { "1" } else { "0" }); | |
| 105 | + | super::log_backend_err("set_config preview_autoplay", self.backend.set_config("preview_autoplay", if self.autoplay { "1" } else { "0" })); | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | 108 | // --- Instrument --- |
| @@ -119,6 +119,23 @@ pub fn resolve_output_names( | |||
| 119 | 119 | names | |
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | + | /// Neutralize path-traversal in a single output filename: strip separators and | |
| 123 | + | /// NUL bytes, and defuse a bare `..` stem. Applied to every output name — | |
| 124 | + | /// including untrusted plugin-supplied overrides — so the primitive is safe | |
| 125 | + | /// regardless of caller. | |
| 126 | + | fn strip_traversal(name: &str) -> String { | |
| 127 | + | // Strip separators and NUL first: with those gone, the name is a single | |
| 128 | + | // path component, so `dir.join(name)` cannot climb out of `dir`. | |
| 129 | + | let out = name.replace(['/', '\\', '\0'], "_"); | |
| 130 | + | // The only remaining escape is a component that IS `.` or `..` — those | |
| 131 | + | // resolve to the current/parent directory on join. Defuse them; every other | |
| 132 | + | // string (including a benign `..foo` or `.._x.wav`) stays in-directory. | |
| 133 | + | if out == "." || out == ".." { | |
| 134 | + | return "_".repeat(out.len()); | |
| 135 | + | } | |
| 136 | + | out | |
| 137 | + | } | |
| 138 | + | ||
| 122 | 139 | #[cfg(test)] | |
| 123 | 140 | mod tests { | |
| 124 | 141 | use super::*; | |
| @@ -189,20 +206,3 @@ mod tests { | |||
| 189 | 206 | assert_eq!(strip_traversal("../x.wav"), ".._x.wav"); | |
| 190 | 207 | } | |
| 191 | 208 | } | |
| 192 | - | ||
| 193 | - | /// Neutralize path-traversal in a single output filename: strip separators and | |
| 194 | - | /// NUL bytes, and defuse a bare `..` stem. Applied to every output name — | |
| 195 | - | /// including untrusted plugin-supplied overrides — so the primitive is safe | |
| 196 | - | /// regardless of caller. | |
| 197 | - | fn strip_traversal(name: &str) -> String { | |
| 198 | - | // Strip separators and NUL first: with those gone, the name is a single | |
| 199 | - | // path component, so `dir.join(name)` cannot climb out of `dir`. | |
| 200 | - | let out = name.replace(['/', '\\', '\0'], "_"); | |
| 201 | - | // The only remaining escape is a component that IS `.` or `..` — those | |
| 202 | - | // resolve to the current/parent directory on join. Defuse them; every other | |
| 203 | - | // string (including a benign `..foo` or `.._x.wav`) stays in-directory. | |
| 204 | - | if out == "." || out == ".." { | |
| 205 | - | return "_".repeat(out.len()); | |
| 206 | - | } | |
| 207 | - | out | |
| 208 | - | } |
| @@ -393,8 +393,7 @@ mod tests { | |||
| 393 | 393 | fn fts_index_stays_synced_through_rename_and_delete() { | |
| 394 | 394 | let (db, vfs_id) = setup_with_samples(); | |
| 395 | 395 | // setup has "kick.wav" (hash1) and "snare.wav" (hash2). | |
| 396 | - | let mut filter = SearchFilter::default(); | |
| 397 | - | filter.text_query = "kick".to_string(); | |
| 396 | + | let filter = SearchFilter { text_query: "kick".to_string(), ..Default::default() }; | |
| 398 | 397 | let nodes = search_in_folder(&db, &filter, vfs_id, None).unwrap(); | |
| 399 | 398 | assert_eq!(nodes.len(), 1, "kick.wav should match before rename"); | |
| 400 | 399 | let kick_id = nodes[0].node.id; | |
| @@ -406,8 +405,7 @@ mod tests { | |||
| 406 | 405 | 0, | |
| 407 | 406 | "kick must no longer match after rename" | |
| 408 | 407 | ); | |
| 409 | - | let mut hat = SearchFilter::default(); | |
| 410 | - | hat.text_query = "hat".to_string(); | |
| 408 | + | let hat = SearchFilter { text_query: "hat".to_string(), ..Default::default() }; | |
| 411 | 409 | assert_eq!( | |
| 412 | 410 | search_in_folder(&db, &hat, vfs_id, None).unwrap().len(), | |
| 413 | 411 | 1, | |
| @@ -426,8 +424,7 @@ mod tests { | |||
| 426 | 424 | #[test] | |
| 427 | 425 | fn text_search_uses_the_fts_trigram_index() { | |
| 428 | 426 | let (db, vfs_id) = setup_with_samples(); | |
| 429 | - | let mut filter = SearchFilter::default(); | |
| 430 | - | filter.text_query = "kick".to_string(); | |
| 427 | + | let filter = SearchFilter { text_query: "kick".to_string(), ..Default::default() }; | |
| 431 | 428 | ||
| 432 | 429 | // Build the same SQL the search uses and confirm its plan reaches the | |
| 433 | 430 | // FTS virtual table rather than a linear scan of vfs_nodes by name. | |
| @@ -465,8 +462,7 @@ mod tests { | |||
| 465 | 462 | insert_fake_sample(&db, &hash); | |
| 466 | 463 | vfs::create_sample_link(&db, vfs_id, None, &format!("kick_{i}.wav"), &hash).unwrap(); | |
| 467 | 464 | } | |
| 468 | - | let mut filter = SearchFilter::default(); | |
| 469 | - | filter.text_query = "kick".to_string(); | |
| 465 | + | let filter = SearchFilter { text_query: "kick".to_string(), ..Default::default() }; | |
| 470 | 466 | let results = search_in_folder(&db, &filter, vfs_id, None).unwrap(); | |
| 471 | 467 | assert_eq!(results.len(), SEARCH_RESULT_LIMIT); | |
| 472 | 468 | } |
| @@ -1453,6 +1453,8 @@ mod tests { | |||
| 1453 | 1453 | // mirror write-through guard), so clear that first to simulate bit-rot. | |
| 1454 | 1454 | let stored_path = store.sample_path(&hash, "wav").unwrap(); | |
| 1455 | 1455 | let mut perms = fs::metadata(&stored_path).unwrap().permissions(); | |
| 1456 | + | // Intentionally clear read-only to simulate bit-rot on a stored blob. | |
| 1457 | + | #[allow(clippy::permissions_set_readonly_false)] | |
| 1456 | 1458 | perms.set_readonly(false); | |
| 1457 | 1459 | fs::set_permissions(&stored_path, perms).unwrap(); | |
| 1458 | 1460 | fs::write(&stored_path, b"corrupted data").unwrap(); | |
| @@ -1589,7 +1591,7 @@ mod tests { | |||
| 1589 | 1591 | let src1 = create_test_file(&dir, "kick.wav", b"kick data"); | |
| 1590 | 1592 | let src2 = create_test_file(&dir, "snare.wav", b"snare data"); | |
| 1591 | 1593 | let hash1 = store.import(&src1, &db).unwrap(); | |
| 1592 | - | let hash2 = store.import(&src2, &db).unwrap(); | |
| 1594 | + | let _hash2 = store.import(&src2, &db).unwrap(); | |
| 1593 | 1595 | ||
| 1594 | 1596 | let vfs_id = crate::vfs::create_vfs(&db, "Lib").unwrap(); | |
| 1595 | 1597 | crate::vfs::create_sample_link(&db, vfs_id, None, "kick.wav", &hash1).unwrap(); |