feat(offline_cache): enforce cache size limit on new track downloads

Pre-flight check_cache_limit reads the current total_size_bytes and
returns an informative error if the cache is already at or over the
configured limit. Wired into v2_cache_track_for_offline (single) and
v2_cache_tracks_batch_for_offline (batch). Re-download paths are not
gated since they target tracks already counted toward the total. The
check intentionally does not predict the new track's size; preventing
cache from growing further once at the limit is enough for v1.

cc
This commit is contained in:
vicrodh committed 2026-05-06 10:23:27 -06:00
1 parent 362f3154ac
commit 1b07036454
2 files changed
+44

No files matched your search

@@ -923,8 +923,13 @@ pub async fn v2_cache_track_for_offline(
let file_path = cache_state.track_file_path(track_id, "flac"); let file_path = cache_state.track_file_path(track_id, "flac");
let file_path_str = file_path.to_string_lossy().to_string(); let file_path_str = file_path.to_string_lossy().to_string();
{ {
let limit_bytes = *cache_state.limit_bytes.lock().await;
let guard = cache_state.db.lock().await; let guard = cache_state.db.lock().await;
let db = guard.as_ref().ok_or("No active session - please log in")?; let db = guard.as_ref().ok_or("No active session - please log in")?;
// Pre-flight: refuse new downloads when the cache is already at or
// above the configured limit (Fix #5d).
let cache_root = std::path::PathBuf::from(cache_state.get_cache_path());
crate::offline_cache::maintenance::check_cache_limit(db, &cache_root, limit_bytes)?;
db.insert_track(&track_info, &file_path_str)?; db.insert_track(&track_info, &file_path_str)?;
} }
@@ -995,8 +1000,14 @@ pub async fn v2_cache_tracks_batch_for_offline(
// Single transactional batch insert // Single transactional batch insert
{ {
let limit_bytes = *cache_state.limit_bytes.lock().await;
let guard = cache_state.db.lock().await; let guard = cache_state.db.lock().await;
let db = guard.as_ref().ok_or("No active session - please log in")?; let db = guard.as_ref().ok_or("No active session - please log in")?;
// Pre-flight: refuse new batch downloads when the cache is already at
// or above the configured limit (Fix #5d). One check covers the whole
// batch; this is intentionally simple and does not predict batch size.
let cache_root = std::path::PathBuf::from(cache_state.get_cache_path());
crate::offline_cache::maintenance::check_cache_limit(db, &cache_root, limit_bytes)?;
let refs: Vec<(&crate::offline_cache::TrackCacheInfo, String)> = batch let refs: Vec<(&crate::offline_cache::TrackCacheInfo, String)> = batch
.iter() .iter()
.map(|(info, path)| (info, path.clone())) .map(|(info, path)| (info, path.clone()))
@@ -41,6 +41,39 @@ pub fn remove_album_cached_tracks(
}) })
} }
/// Pre-flight cache-limit check executed before queuing new downloads.
///
/// Returns `Ok(())` when:
/// - `limit_bytes` is `None` (no limit configured); OR
/// - the current `total_size_bytes` from the cache stats is strictly below the
/// configured limit.
///
/// Returns `Err(message)` when the cache has already met or exceeded the
/// configured limit. The message is suitable for surfacing to end users via a
/// toast.
///
/// Intentionally simple: it does not predict the new track's size. Sufficient
/// for v1 — the user gets feedback the moment they hit the limit and can free
/// space or raise the limit through the offline cache manager.
pub fn check_cache_limit(
db: &OfflineCacheDb,
offline_root: &Path,
limit_bytes: Option<u64>,
) -> Result<(), String> {
let limit = match limit_bytes {
Some(value) => value,
None => return Ok(()),
};
let stats = db.get_stats(&offline_root.to_string_lossy(), Some(limit))?;
if stats.total_size_bytes >= limit {
return Err(
"Offline cache limit reached. Free space or raise the limit in the offline cache manager."
.to_string(),
);
}
Ok(())
}
/// Filters tracks targeted by re-download: skip in-flight Downloading, /// Filters tracks targeted by re-download: skip in-flight Downloading,
/// optionally restrict to Failed only. /// optionally restrict to Failed only.
pub fn select_redownload_targets( pub fn select_redownload_targets(