Skip to main content

max / audiofiles

17.4 KB · 528 lines History Blame Raw
1 //! Backend abstraction for database and store access.
2 //!
3 //! The [`Backend`] trait defines all operations that BrowserState needs from
4 //! the data layer. Two implementations exist:
5 //!
6 //! - [`DirectBackend`] — wraps `Mutex<Database>` + `SampleStore`, calls core functions directly.
7 //! Used in tests, standalone mode, and as a reference implementation.
8 //! - `DaemonBackend` (Phase D) — forwards calls to the daemon over Unix socket via JSON-RPC.
9
10 pub mod direct;
11
12 use std::path::{Path, PathBuf};
13
14 use audiofiles_core::analysis::config::AnalysisConfig;
15 use audiofiles_core::analysis::suggest::TagSuggestion;
16 use audiofiles_core::analysis::waveform::WaveformData;
17 use audiofiles_core::analysis::AnalysisResult;
18 use audiofiles_core::edit::EditOperation;
19 use audiofiles_core::export::profile::DeviceProfileSummary;
20 use audiofiles_core::export::{ExportConfig, ExportItem};
21 use audiofiles_core::search::SearchFilter;
22 use audiofiles_core::collections::Collection;
23 use audiofiles_core::vfs::{Vfs, VfsNode, VfsNodeWithAnalysis};
24 use audiofiles_core::{CollectionId, NodeId, VfsId};
25
26 pub use direct::DirectBackend;
27
28 /// Result type for backend operations.
29 pub type BackendResult<T> = Result<T, BackendError>;
30
31 /// Unified error type for backend operations.
32 #[derive(Debug, thiserror::Error)]
33 pub enum BackendError {
34 #[error("{0}")]
35 Core(#[from] audiofiles_core::error::CoreError),
36
37 #[error("{0}")]
38 Other(String),
39 }
40
41 /// Events from long-running background operations (import, analysis, export).
42 ///
43 /// Unifies the separate ImportEvent, WorkerEvent, and ExportEvent types so that
44 /// a single `poll_events()` call can drain all pending worker notifications.
45 #[derive(Debug, serde::Serialize, serde::Deserialize)]
46 pub enum BackendEvent {
47 // Import events
48 ImportWalkProgress {
49 count: usize,
50 total_bytes: u64,
51 },
52 ImportWalkComplete {
53 total: usize,
54 total_bytes: u64,
55 },
56 ImportProgress {
57 completed: usize,
58 total: usize,
59 current_name: String,
60 },
61 ImportFileError {
62 path: String,
63 error: String,
64 },
65 ImportComplete {
66 imported: Vec<(String, String)>,
67 total_files: usize,
68 errors: usize,
69 duplicates: usize,
70 folders: Vec<ImportedFolderDesc>,
71 },
72
73 // Analysis events
74 AnalysisProgress {
75 completed: usize,
76 total: usize,
77 current_name: String,
78 },
79 AnalysisSampleDone {
80 result: Box<AnalysisResult>,
81 suggestions: Vec<TagSuggestion>,
82 },
83 AnalysisSampleError {
84 hash: String,
85 error: String,
86 },
87 AnalysisBatchComplete,
88
89 // Export events
90 ExportProgress {
91 completed: usize,
92 total: usize,
93 current_name: String,
94 },
95 ExportComplete {
96 total: usize,
97 errors: Vec<(String, String)>,
98 },
99
100 // Cleanup events
101 CleanupProgress {
102 completed: usize,
103 total: usize,
104 current_name: String,
105 },
106 CleanupComplete {
107 removed: usize,
108 errors: usize,
109 },
110
111 // Edit events
112 EditStarted {
113 hash: String,
114 },
115 EditComplete {
116 source_hash: String,
117 result_path: std::path::PathBuf,
118 operation: EditOperation,
119 },
120 EditError {
121 hash: String,
122 error: String,
123 },
124 }
125
126 /// Serializable description of an imported folder (for IPC).
127 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
128 pub struct ImportedFolderDesc {
129 pub name: String,
130 pub samples: Vec<(String, String)>,
131 }
132
133 /// Serializable description of an import strategy (for IPC).
134 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
135 pub enum ImportStrategyDesc {
136 Flat { vfs_id: VfsId, parent_id: Option<NodeId> },
137 NewVfs { vfs_name: String },
138 MergeIntoVfs { vfs_id: VfsId, parent_id: Option<NodeId> },
139 }
140
141 /// Serializable description of an export item (for IPC).
142 pub type ExportItemDesc = ExportItem;
143
144 /// Serializable description of an export config (for IPC).
145 pub type ExportConfigDesc = ExportConfig;
146
147 /// Aggregate storage statistics for a vault.
148 #[derive(Debug, Clone, Default)]
149 pub struct StorageStats {
150 pub sample_count: u64,
151 pub total_bytes: u64,
152 pub db_bytes: u64,
153 }
154
155 /// The core abstraction separating UI from data access.
156 ///
157 /// Every method is synchronous and blocking. The trait is `Send + Sync` so it
158 /// can live inside `BrowserState` (which must be Send + Sync for nih-plug).
159 pub trait Backend: Send + Sync {
160 // --- VFS ---
161
162 /// List all VFS roots, ordered alphabetically.
163 fn list_vfs(&self) -> BackendResult<Vec<Vfs>>;
164
165 /// Create a new VFS root. Returns the new VFS ID.
166 fn create_vfs(&self, name: &str) -> BackendResult<VfsId>;
167
168 /// Rename a VFS root.
169 fn rename_vfs(&self, id: VfsId, new_name: &str) -> BackendResult<()>;
170
171 /// Delete a VFS root and all its nodes.
172 fn delete_vfs(&self, id: VfsId) -> BackendResult<()>;
173
174 /// List children of a directory with analysis data joined in.
175 fn list_children_enriched(
176 &self,
177 vfs_id: VfsId,
178 parent_id: Option<NodeId>,
179 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
180
181 /// List direct children (without analysis data).
182 fn list_children(
183 &self,
184 vfs_id: VfsId,
185 parent_id: Option<NodeId>,
186 ) -> BackendResult<Vec<VfsNode>>;
187
188 /// Create a directory node. Returns the new node ID.
189 fn create_directory(
190 &self,
191 vfs_id: VfsId,
192 parent_id: Option<NodeId>,
193 name: &str,
194 ) -> BackendResult<NodeId>;
195
196 /// Create a sample link node. Returns the new node ID.
197 fn create_sample_link(
198 &self,
199 vfs_id: VfsId,
200 parent_id: Option<NodeId>,
201 name: &str,
202 sample_hash: &str,
203 ) -> BackendResult<NodeId>;
204
205 /// Fetch a single VFS node by ID.
206 fn get_node(&self, id: NodeId) -> BackendResult<VfsNode>;
207
208 /// Walk from a node up to the VFS root, returning root→node path.
209 fn get_breadcrumb(&self, node_id: NodeId) -> BackendResult<Vec<VfsNode>>;
210
211 /// Rename a VFS node.
212 fn rename_node(&self, id: NodeId, new_name: &str) -> BackendResult<()>;
213
214 /// Move a VFS node to a new parent.
215 fn move_node(&self, id: NodeId, new_parent_id: Option<NodeId>) -> BackendResult<()>;
216
217 /// Delete a VFS node (cascades to children).
218 fn delete_node(&self, id: NodeId) -> BackendResult<()>;
219
220 /// Re-insert a previously deleted node (for undo).
221 fn restore_node(&self, node: &VfsNode) -> BackendResult<()>;
222
223 /// Recursively collect a node and all its descendants.
224 fn collect_subtree(&self, node_id: NodeId) -> BackendResult<Vec<VfsNode>>;
225
226 /// List all directories in a VFS with full paths.
227 fn list_all_directories(&self, vfs_id: VfsId) -> BackendResult<Vec<(NodeId, String)>>;
228
229 /// Find VFS nodes by sample hashes within a specific VFS.
230 fn find_nodes_by_hashes(
231 &self,
232 vfs_id: VfsId,
233 hashes: &[&str],
234 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
235
236 // --- Tags ---
237
238 /// Add a tag to a sample.
239 fn add_tag(&self, hash: &str, tag: &str) -> BackendResult<()>;
240
241 /// Remove a tag from a sample.
242 fn remove_tag(&self, hash: &str, tag: &str) -> BackendResult<()>;
243
244 /// Get all tags for a sample.
245 fn get_sample_tags(&self, hash: &str) -> BackendResult<Vec<String>>;
246
247 /// List all tags in the database.
248 fn list_all_tags(&self) -> BackendResult<Vec<String>>;
249
250 /// Add a tag to multiple samples. Returns count of tags added.
251 fn bulk_add_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize>;
252
253 /// Remove a tag from multiple samples. Returns count of tags removed.
254 fn bulk_remove_tag(&self, hashes: &[&str], tag: &str) -> BackendResult<usize>;
255
256 /// Rename a tag everywhere it appears. Returns count of samples affected.
257 fn rename_tag_globally(&self, old_tag: &str, new_tag: &str) -> BackendResult<usize>;
258
259 /// Count samples that carry an exact tag (used for rename / remove preview).
260 fn count_samples_with_tag(&self, tag: &str) -> BackendResult<usize>;
261
262 /// Remove a tag from every sample that carries it. Returns count of samples affected.
263 fn remove_tag_globally(&self, tag: &str) -> BackendResult<usize>;
264
265 // --- Search ---
266
267 /// Search within a specific VFS folder.
268 fn search_in_folder(
269 &self,
270 filter: &SearchFilter,
271 vfs_id: VfsId,
272 parent_id: Option<NodeId>,
273 ) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
274
275 /// Search globally across all VFS roots.
276 fn search_global(&self, filter: &SearchFilter) -> BackendResult<Vec<VfsNodeWithAnalysis>>;
277
278 // --- Smart folders ---
279
280 // --- Collections ---
281
282 /// List all collections with member counts.
283 fn list_collections(&self) -> BackendResult<Vec<Collection>>;
284
285 /// Create a manual collection. Returns the new ID.
286 fn create_collection(&self, name: &str, description: Option<&str>) -> BackendResult<CollectionId>;
287
288 /// Create a dynamic collection (saved search). Returns the new ID.
289 fn create_dynamic_collection(&self, name: &str, filter: &SearchFilter) -> BackendResult<CollectionId>;
290
291 /// Rename a collection.
292 fn rename_collection(&self, id: CollectionId, new_name: &str) -> BackendResult<()>;
293
294 /// Delete a collection (cascades members).
295 fn delete_collection(&self, id: CollectionId) -> BackendResult<()>;
296
297 /// Add a sample to a collection (no-op if already present).
298 fn add_to_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
299
300 /// Remove a sample from a collection.
301 fn remove_from_collection(&self, collection_id: CollectionId, sample_hash: &str) -> BackendResult<()>;
302
303 /// List sample hashes in a collection.
304 fn list_collection_members(&self, collection_id: CollectionId) -> BackendResult<Vec<String>>;
305
306 /// Get all collections containing a given sample.
307 fn get_sample_collections(&self, sample_hash: &str) -> BackendResult<Vec<Collection>>;
308
309 // --- Analysis ---
310
311 /// Get the full analysis result for a sample, if it exists.
312 fn get_analysis(&self, hash: &str) -> BackendResult<Option<AnalysisResult>>;
313
314 /// Save an analysis result to the database.
315 fn save_analysis(&self, result: &AnalysisResult) -> BackendResult<()>;
316
317 /// Get waveform display data for a sample.
318 fn get_waveform(&self, hash: &str) -> BackendResult<Option<WaveformData>>;
319
320 // --- Similarity ---
321
322 /// Find samples similar to the given hash.
323 fn find_similar(
324 &self,
325 hash: &str,
326 limit: usize,
327 ) -> BackendResult<Vec<audiofiles_core::similarity::SimilarResult>>;
328
329 /// Find near-duplicate samples by fingerprint comparison.
330 fn find_near_duplicates(
331 &self,
332 hash: &str,
333 limit: usize,
334 ) -> BackendResult<Vec<audiofiles_core::fingerprint::DuplicateResult>>;
335
336 // --- Store ---
337
338 /// Import a file into the content-addressed store. Returns the hash.
339 fn import_file(&self, path: &Path) -> BackendResult<String>;
340
341 /// Get the filesystem path for a stored sample.
342 fn sample_path(&self, hash: &str, ext: &str) -> BackendResult<PathBuf>;
343
344 /// Look up the file extension for a sample hash.
345 fn sample_extension(&self, hash: &str) -> BackendResult<String>;
346
347 /// Look up the original filename for a sample hash.
348 fn sample_original_name(&self, hash: &str) -> BackendResult<String>;
349
350 /// Remove a sample from the store and database (CASCADE handles VFS nodes, tags, analysis).
351 fn remove_sample(&self, hash: &str) -> BackendResult<()>;
352
353 /// Remove samples no longer referenced by any VFS node. Returns count removed.
354 fn remove_orphaned_samples(&self) -> BackendResult<usize>;
355
356 /// Look up the source_path for an loose-files mode sample. Returns None for normal samples.
357 fn sample_source_path(&self, hash: &str) -> BackendResult<Option<String>>;
358
359 /// Relocate an loose-files mode sample to a new path (verifies hash match).
360 fn relocate_sample(&self, hash: &str, new_path: &Path) -> BackendResult<()>;
361
362 /// Check integrity of loose-files mode samples. Returns (valid, missing).
363 fn check_vault_integrity(&self) -> BackendResult<(usize, usize)>;
364
365 /// Delete all loose-files mode samples whose source files are missing. Returns count purged.
366 fn purge_missing_loose_files(&self) -> BackendResult<usize>;
367
368 /// Search `search_root` for files matching missing loose-files samples by hash;
369 /// update their source_path to the new location. Returns
370 /// `(relocated, still_missing)` so the caller can decide whether to prompt
371 /// again with a different directory.
372 fn relocate_missing_loose_files(
373 &self,
374 search_root: &std::path::Path,
375 ) -> BackendResult<(usize, usize)>;
376
377 // --- Export ---
378
379 /// Collect export items from a VFS subtree.
380 fn collect_export_items(
381 &self,
382 vfs_id: VfsId,
383 parent_id: Option<NodeId>,
384 ) -> BackendResult<Vec<ExportItem>>;
385
386 /// Populate tags on export items.
387 fn enrich_export_with_tags(&self, items: &mut [ExportItem]) -> BackendResult<()>;
388
389 // --- Device profiles ---
390
391 /// List available device profiles for device-aware export.
392 fn list_device_profiles(&self) -> BackendResult<Vec<DeviceProfileSummary>>;
393
394 // --- Config ---
395
396 /// Get a user config value by key.
397 fn get_config(&self, key: &str) -> BackendResult<Option<String>>;
398
399 /// Set a user config value.
400 fn set_config(&self, key: &str, value: &str) -> BackendResult<()>;
401
402 /// Delete a user config value by key. No-op if the key does not exist.
403 fn delete_config(&self, key: &str) -> BackendResult<()>;
404
405 /// Set whether a VFS should sync audio file blobs to cloud.
406 fn set_vfs_sync_files(&self, id: VfsId, enabled: bool) -> BackendResult<()>;
407
408 /// Get whether a VFS has audio file blob syncing enabled.
409 fn get_vfs_sync_files(&self, id: VfsId) -> BackendResult<bool>;
410
411 // --- VFS Mirror ---
412
413 /// Synchronise the VFS mirror directory with the current VFS state.
414 /// Returns `(dirs_created, links_created, entries_removed)`.
415 fn sync_vfs_mirror(&self, mirror_root: &Path) -> BackendResult<(usize, usize, usize)>;
416
417 // --- Long-running operations ---
418
419 /// Start a folder import in the background.
420 fn start_import(
421 &self,
422 source: &Path,
423 strategy: ImportStrategyDesc,
424 ) -> BackendResult<()>;
425
426 /// Start analysis on a batch of samples.
427 fn start_analysis(
428 &self,
429 samples: Vec<(String, String)>,
430 config: AnalysisConfig,
431 ) -> BackendResult<()>;
432
433 /// Start an export operation.
434 fn start_export(
435 &self,
436 items: Vec<ExportItemDesc>,
437 config: ExportConfigDesc,
438 ) -> BackendResult<()>;
439
440 /// Cancel a running import.
441 fn cancel_import(&self) -> BackendResult<()>;
442
443 /// Cancel a running analysis.
444 fn cancel_analysis(&self) -> BackendResult<()>;
445
446 /// Cancel a running export.
447 fn cancel_export(&self) -> BackendResult<()>;
448
449 /// Start an edit operation on a sample.
450 fn start_edit(&self, hash: &str, ext: &str, operation: EditOperation) -> BackendResult<()>;
451
452 /// Cancel a running edit.
453 fn cancel_edit(&self) -> BackendResult<()>;
454
455 /// Start background orphaned sample cleanup.
456 fn start_cleanup(&self) -> BackendResult<()>;
457
458 /// Cancel a running cleanup.
459 fn cancel_cleanup(&self) -> BackendResult<()>;
460
461 /// Record an edit in the edit_history table.
462 fn record_edit_history(
463 &self,
464 source_hash: &str,
465 result_hash: &str,
466 operation: &EditOperation,
467 ) -> BackendResult<()>;
468
469 /// Delete the most recent `edit_history` row matching this (source, result)
470 /// pair. Used by `BrowserState::undo_last_edit` to reverse a previously
471 /// recorded edit when the user clicks the inline Undo affordance.
472 fn delete_edit_history(
473 &self,
474 source_hash: &str,
475 result_hash: &str,
476 ) -> BackendResult<()>;
477
478 /// Get aggregate storage statistics for the current vault.
479 fn storage_stats(&self) -> BackendResult<StorageStats>;
480
481 /// Per-VFS storage stats: `(unique_sample_count, total_bytes)`. Surfaced in
482 /// the sync panel's per-VFS toggle rows so the user can see upload size
483 /// before enabling blob sync for that vault.
484 fn vfs_storage_stats(&self, vfs_id: audiofiles_core::VfsId) -> BackendResult<(u64, u64)>;
485
486 /// Non-blocking poll for worker events.
487 fn poll_events(&self) -> Vec<BackendEvent>;
488 }
489
490 #[cfg(test)]
491 mod tests {
492 use super::*;
493
494 #[test]
495 fn backend_error_from_core() {
496 let core_err = audiofiles_core::error::CoreError::NodeNotFound(NodeId::from(42));
497 let backend_err: BackendError = core_err.into();
498 assert!(backend_err.to_string().contains("42"));
499 }
500
501 #[test]
502 fn backend_error_other() {
503 let err = BackendError::Other("test error".to_string());
504 assert_eq!(err.to_string(), "test error");
505 }
506
507 #[test]
508 fn imported_folder_desc_serializes() {
509 let desc = ImportedFolderDesc {
510 name: "Drums".to_string(),
511 samples: vec![("hash1".to_string(), "wav".to_string())],
512 };
513 let json = serde_json::to_string(&desc).unwrap();
514 assert!(json.contains("Drums"));
515 }
516
517 #[test]
518 fn import_strategy_desc_variants() {
519 let flat = ImportStrategyDesc::Flat { vfs_id: VfsId::from(1), parent_id: None };
520 let json = serde_json::to_string(&flat).unwrap();
521 assert!(json.contains("Flat"));
522
523 let new_vfs = ImportStrategyDesc::NewVfs { vfs_name: "Test".to_string() };
524 let json = serde_json::to_string(&new_vfs).unwrap();
525 assert!(json.contains("Test"));
526 }
527 }
528