Skip to content

Remove empty folders left behind by cache eviction - #202

Open
Brandon-Haney wants to merge 2 commits into
StudioNirin:mainfrom
Brandon-Haney:fix/issue-196-empty-folder-cleanup
Open

Remove empty folders left behind by cache eviction#202
Brandon-Haney wants to merge 2 commits into
StudioNirin:mainfrom
Brandon-Haney:fix/issue-196-empty-folder-cleanup

Conversation

@Brandon-Haney

Copy link
Copy Markdown
Collaborator

Closes #196

Reported in #196 (comment): evicting files leaves the folders behind on the cache drive.

Turns out only the normal move-to-array path was cleaning up after itself. Three other paths deleted the cache file and stopped there:

  • the per-file and bulk Evict buttons on the Cached Files page
  • the maintenance Evict action (delegates to the same service method)
  • automatic smart/FIFO eviction during a run

The last one is probably the bigger source of the backlog, since it runs unattended on every run that crosses the threshold, so it has been quietly accumulating.

While wiring this up I found two other things worth mentioning:

Multi-pool setups were being skipped even on the working path. The boundary for the walk-up was the single cache_dir, checked with startswith. If a mapping lives on a different pool (say /mnt/ssd_cache while cache_dir is /mnt/cache) that check fails immediately, so cleanup silently did nothing there. The boundary now comes from the owning path mapping instead.

The startswith had no separator guard, so /mnt/cache_downloads counted as being inside /mnt/cache. That is a cleanup walking into a directory it does not own. It now requires a separator match.

One deliberate behavior change: a mapping's own cache_path is preserved even when it ends up empty. It is a configured caching destination rather than a folder PlexCache created, and removing it would leave the next run without somewhere to write.

Since per-file cleanup only ever sees folders it empties itself, existing installs keep whatever backlog they already have. Rather than ask everyone to clear it by hand, _backfill_empty_folder_cleanup() sweeps the cache mappings once on the first run after upgrade, records a marker in data/migrations.json, and never runs again. It logs one summary line and is skipped on dry runs and when cleanup_empty_folders is off. Sweep and marker failures are logged but non-fatal, since housekeeping should not be able to fail a run.

Also consolidated the duplicate sweep logic: cleanup_empty_parent_folders() and sweep_empty_folders() now live in core/system_utils.py, and FileMover._cleanup_empty_parent_folders() plus MaintenanceService._cleanup_empty_directories() are thin wrappers over them.

Test steps

Manual eviction:

  1. Cache a movie, confirm the folder exists under the cache path.
  2. Evict it from the Cached Files page (or select several and use bulk evict).
  3. The movie folder should be gone from the cache drive, and the file should still be on the array.

TV, to check the walk-up:

  1. Cache one episode of a show that has nothing else cached.
  2. Evict it. Both the season and show folders should go.
  3. Cache two episodes, evict one. The season folder should stay, along with the other episode.

Backfill:

  1. On an install that already has empty folders on the cache drive, start a run.
  2. The log should show [CLEANUP] Removed N empty folder(s) left by earlier evictions shortly after startup, and data/migrations.json should gain an empty_folder_backfill key.
  3. Start another run. No cleanup line, and the marker is unchanged.

Opting out:

  1. Turn off cleanup_empty_folders in Settings, then evict a file. The folder should stay.
  2. A dry run should not sweep or write the marker.

Should be safe to leave alone: dot-directories like .Trash and .Recycle.Bin, anything in excluded_folders, disabled or non-cacheable mappings, and the mapping roots themselves.

Adds 36 tests across tests/test_empty_folder_cleanup.py, tests/test_evict_folder_cleanup.py and tests/test_empty_folder_backfill.py.

Evicting a file deleted the cache copy and stopped there, so the movie
folder (and the show/season chain for TV) stayed on the cache drive. Only
the normal move-to-array path cleaned up after itself.

Three paths were affected: the per-file and bulk Evict buttons on the
Cached Files page, the maintenance Evict action (which delegates to the
same service method), and automatic smart/FIFO eviction during a run.

- Adds `cleanup_empty_parent_folders()` and `resolve_cache_boundary()` to
  core/system_utils.py as the canonical implementations, and calls them
  from CacheService.evict_file() and PlexCacheApp._run_eviction().
- FileMover._cleanup_empty_parent_folders() is now a thin wrapper over the
  shared helper. Its boundary comes from the owning path mapping rather
  than the single cache_dir, so a mapping on a second pool
  (/mnt/ssd_cache alongside /mnt/cache) is cleaned instead of skipped, and
  the mapping's own cache_path is preserved as a caching destination.
- The boundary comparison now requires a separator match, so
  /mnt/cache_downloads is no longer treated as living inside /mnt/cache.
- Honours the existing cleanup_empty_folders setting on every path.
Per-file cleanup only ever sees folders it empties itself, so installs
upgrading from a version where eviction skipped cleanup keep whatever
backlog they already accumulated. Existing users would otherwise have to
clear it by hand.

- `_backfill_empty_folder_cleanup()` sweeps the enabled, cacheable cache
  mappings once on the first run after upgrade, then records a marker in
  data/migrations.json so it never repeats. Reports one summary line,
  `[CLEANUP] Removed N empty folder(s) left by earlier evictions`, with
  per-folder detail at DEBUG.
- Skipped on dry runs and when cleanup_empty_folders is off. Sweep and
  marker failures are logged and non-fatal — housekeeping shouldn't be
  able to fail a run.
- Adds `sweep_empty_folders()` to core/system_utils.py as the canonical
  full-tree sweep, and points MaintenanceService._cleanup_empty_directories()
  at it so there's one implementation. Dot-directories (.Trash,
  .Recycle.Bin) and configured excluded_folders are left alone, and the
  mapping roots themselves are never removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Another Unraid Setup issue

1 participant