From 480e32ba04b5995fe6a58c21456f7159be9845e7 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 8 May 2026 11:40:37 +0200 Subject: [PATCH] refactor(cache): extract dir_size + prune_empty_dirs into fs_utils Lift the duplicated `dir_size` walker and the empty-directory upward-prune loop out of cache/offline.rs and cache/hot.rs into a shared cache/fs_utils.rs module. Behaviour-preserving: same boundary semantics, same skip-on-error policy, same `remove_dir`-only pruning (never remove_dir_all). Net delta: -76 LOC across cache/, single source of truth for cache cleanup. --- src-tauri/src/lib_commands/cache/fs_utils.rs | 53 ++++++++++++++++++++ src-tauri/src/lib_commands/cache/hot.rs | 45 ++--------------- src-tauri/src/lib_commands/cache/mod.rs | 1 + src-tauri/src/lib_commands/cache/offline.rs | 46 ++--------------- 4 files changed, 61 insertions(+), 84 deletions(-) create mode 100644 src-tauri/src/lib_commands/cache/fs_utils.rs diff --git a/src-tauri/src/lib_commands/cache/fs_utils.rs b/src-tauri/src/lib_commands/cache/fs_utils.rs new file mode 100644 index 00000000..640ee82d --- /dev/null +++ b/src-tauri/src/lib_commands/cache/fs_utils.rs @@ -0,0 +1,53 @@ +use std::path::Path; + +/// Recursively sums the size of all files under `root`. +/// Missing roots, unreadable directories, and unreadable files are silently skipped. +pub(crate) fn dir_size_recursive(root: &Path) -> u64 { + if !root.exists() { + return 0; + } + let mut total: u64 = 0; + let mut stack = vec![root.to_path_buf()]; + while let Some(dir) = stack.pop() { + let rd = match std::fs::read_dir(&dir) { + Ok(r) => r, + Err(_) => continue, + }; + for entry in rd.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if let Ok(meta) = std::fs::metadata(&path) { + total += meta.len(); + } + } + } + total +} + +/// Walks upward from `start_dir`, removing each empty directory using `remove_dir` +/// (never `remove_dir_all`). Stops as soon as a non-empty directory is hit, the +/// boundary is reached, or removal fails. +/// +/// `boundary` is never removed and is treated as a hard stop. If `start_dir` is +/// not under `boundary`, the function is a no-op. +pub(crate) fn prune_empty_dirs_up_to(start_dir: &Path, boundary: &Path) { + let mut current = Some(start_dir.to_path_buf()); + while let Some(dir) = current { + if dir == boundary || !dir.starts_with(boundary) { + break; + } + match std::fs::read_dir(&dir) { + Ok(mut entries) => { + if entries.next().is_some() { + break; + } + if std::fs::remove_dir(&dir).is_err() { + break; + } + current = dir.parent().map(|p| p.to_path_buf()); + } + Err(_) => break, + } + } +} diff --git a/src-tauri/src/lib_commands/cache/hot.rs b/src-tauri/src/lib_commands/cache/hot.rs index 537532b0..476c5800 100644 --- a/src-tauri/src/lib_commands/cache/hot.rs +++ b/src-tauri/src/lib_commands/cache/hot.rs @@ -172,31 +172,8 @@ pub(crate) async fn promote_stream_cache_to_hot_cache( #[tauri::command] pub(crate) async fn get_hot_cache_size(custom_dir: Option, app: tauri::AppHandle) -> u64 { - fn dir_size(root: std::path::PathBuf) -> u64 { - if !root.exists() { - return 0; - } - let mut total: u64 = 0; - let mut stack = vec![root]; - while let Some(dir) = stack.pop() { - let rd = match std::fs::read_dir(&dir) { - Ok(r) => r, - Err(_) => continue, - }; - for entry in rd.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - } else if let Ok(meta) = std::fs::metadata(&path) { - total += meta.len(); - } - } - } - total - } - resolve_hot_cache_root(custom_dir, &app) - .map(|root| dir_size(root)) + .map(|root| super::fs_utils::dir_size_recursive(&root)) .unwrap_or(0) } @@ -223,24 +200,8 @@ pub(crate) async fn delete_hot_cache_track( ); let boundary = resolve_hot_cache_root(custom_dir, &app)?; - - let mut current = file_path.parent().map(|p| p.to_path_buf()); - while let Some(dir) = current { - if dir == boundary || !dir.starts_with(&boundary) { - break; - } - match std::fs::read_dir(&dir) { - Ok(mut entries) => { - if entries.next().is_some() { - break; - } - if std::fs::remove_dir(&dir).is_err() { - break; - } - current = dir.parent().map(|p| p.to_path_buf()); - } - Err(_) => break, - } + if let Some(parent) = file_path.parent() { + super::fs_utils::prune_empty_dirs_up_to(parent, &boundary); } Ok(()) diff --git a/src-tauri/src/lib_commands/cache/mod.rs b/src-tauri/src/lib_commands/cache/mod.rs index 011e3e72..91fa4d2b 100644 --- a/src-tauri/src/lib_commands/cache/mod.rs +++ b/src-tauri/src/lib_commands/cache/mod.rs @@ -1,5 +1,6 @@ use super::*; +mod fs_utils; mod offline; mod downloads; mod hot; diff --git a/src-tauri/src/lib_commands/cache/offline.rs b/src-tauri/src/lib_commands/cache/offline.rs index 1379b558..a8a41ff3 100644 --- a/src-tauri/src/lib_commands/cache/offline.rs +++ b/src-tauri/src/lib_commands/cache/offline.rs @@ -145,37 +145,16 @@ pub(crate) async fn download_track_offline( /// Returns the total size in bytes of all files in the offline cache directory (and optional custom dir). #[tauri::command] pub(crate) async fn get_offline_cache_size(custom_dir: Option, app: tauri::AppHandle) -> u64 { - fn dir_size(root: std::path::PathBuf) -> u64 { - if !root.exists() { return 0; } - let mut total: u64 = 0; - let mut stack = vec![root]; - while let Some(dir) = stack.pop() { - let rd = match std::fs::read_dir(&dir) { - Ok(r) => r, - Err(_) => continue, - }; - for entry in rd.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - } else if let Ok(meta) = std::fs::metadata(&path) { - total += meta.len(); - } - } - } - total - } - let default_dir = match app.path().app_data_dir() { Ok(d) => d.join("psysonic-offline"), Err(_) => return 0, }; - let mut total = dir_size(default_dir); + let mut total = super::fs_utils::dir_size_recursive(&default_dir); if let Some(cd) = custom_dir { let custom = std::path::PathBuf::from(cd); if custom != std::path::PathBuf::from("") { - total += dir_size(custom); + total += super::fs_utils::dir_size_recursive(&custom); } } total @@ -208,25 +187,8 @@ pub(crate) async fn delete_offline_track( .join("psysonic-offline") }; - // Walk upward, pruning directories that have become empty. - // Stops as soon as a non-empty directory or the boundary is reached. - let mut current = file_path.parent().map(|p| p.to_path_buf()); - while let Some(dir) = current { - if dir == boundary || !dir.starts_with(&boundary) { - break; - } - match std::fs::read_dir(&dir) { - Ok(mut entries) => { - if entries.next().is_some() { - break; // Directory still has contents — stop pruning. - } - if std::fs::remove_dir(&dir).is_err() { - break; // Could not remove (e.g. permissions) — stop. - } - current = dir.parent().map(|p| p.to_path_buf()); - } - Err(_) => break, - } + if let Some(parent) = file_path.parent() { + super::fs_utils::prune_empty_dirs_up_to(parent, &boundary); } Ok(())