[PR #5148] Fix directory symlink support for scanning, browsing, and watching #4444

Open
opened 2026-04-25 00:19:46 +02:00 by adam · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/advplyr/audiobookshelf/pull/5148
Author: @jacobtruman
Created: 3/24/2026
Status: 🔄 Open

Base: masterHead: fix/symlink-directory-support


📝 Commits (1)

  • 08c568a Fix issues with symlink directories

📊 Changes

5 files changed (+18 additions, -8 deletions)

View changed files

📝 server/Watcher.js (+2 -1)
📝 server/libs/watcher/utils.js (+2 -2)
📝 server/libs/watcher/watcher_handler.js (+2 -2)
📝 server/utils/fileUtils.js (+10 -2)
📝 test/server/utils/fileUtils.test.js (+2 -1)

📄 Description

Brief summary

Fix directory symlinks so they work correctly across library folder browsing, library scanning, and file watching.

Which issue is fixed?

In-depth Description

Directory symlinks were broken in three areas:

1. Library folder browsing (getDirectoriesInPath) — Used lstat() which identifies symlinks as symlinks rather than directories. Symlinked directories were invisible when selecting library folders in the UI. Fixed by keeping lstat for regular directories (preserving existing behavior) and adding a secondary stat() check when an entry is a symlink, to determine if the target is a directory.

2. Library scanning (recurseFiles) — Used realPath: true in recursive-readdir-async, which resolves symlinks to their real paths via fs.realpath(). Files inside a symlinked directory (e.g., /audiobooks/series1 → /data/series1) would get paths like /data/series1/book.mp3 instead of /audiobooks/series1/book.mp3. The relPathToReplace logic couldn't match the real path prefix, causing the scanner to find zero files. Fixed by setting realPath: falsefs.readdir and fs.stat already follow symlinks transparently at the OS level.

3. File watcher — The bundled watcher library (tiny-readdir) supports a followSymlinks option, but it was never passed through. Symlinked directories were detected but not traversed, so changes inside them didn't trigger watch events. Fixed by passing followSymlinks: true from Watcher.js and threading the option through utils.readdirtiny-readdir at both call sites in watcher_handler.js.

How have you tested this?

  • All 335 existing tests pass (npm test)
  • Updated the recurseFiles test mock to handle trailing slashes (since realPath: false no longer strips them)
  • Manual testing with directory symlinks for library folder selection, scanning, and file watching

Screenshots

N/A — server-side changes only


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/advplyr/audiobookshelf/pull/5148 **Author:** [@jacobtruman](https://github.com/jacobtruman) **Created:** 3/24/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `fix/symlink-directory-support` --- ### 📝 Commits (1) - [`08c568a`](https://github.com/advplyr/audiobookshelf/commit/08c568a3fcd0a29e9de7482ec50f393750055317) Fix issues with symlink directories ### 📊 Changes **5 files changed** (+18 additions, -8 deletions) <details> <summary>View changed files</summary> 📝 `server/Watcher.js` (+2 -1) 📝 `server/libs/watcher/utils.js` (+2 -2) 📝 `server/libs/watcher/watcher_handler.js` (+2 -2) 📝 `server/utils/fileUtils.js` (+10 -2) 📝 `test/server/utils/fileUtils.test.js` (+2 -1) </details> ### 📄 Description ## Brief summary Fix directory symlinks so they work correctly across library folder browsing, library scanning, and file watching. ## Which issue is fixed? <!-- Related: directory symlinks were not properly supported in multiple code paths --> ## In-depth Description Directory symlinks were broken in three areas: **1. Library folder browsing (`getDirectoriesInPath`)** — Used `lstat()` which identifies symlinks as symlinks rather than directories. Symlinked directories were invisible when selecting library folders in the UI. Fixed by keeping `lstat` for regular directories (preserving existing behavior) and adding a secondary `stat()` check when an entry is a symlink, to determine if the target is a directory. **2. Library scanning (`recurseFiles`)** — Used `realPath: true` in `recursive-readdir-async`, which resolves symlinks to their real paths via `fs.realpath()`. Files inside a symlinked directory (e.g., `/audiobooks/series1 → /data/series1`) would get paths like `/data/series1/book.mp3` instead of `/audiobooks/series1/book.mp3`. The `relPathToReplace` logic couldn't match the real path prefix, causing the scanner to find zero files. Fixed by setting `realPath: false` — `fs.readdir` and `fs.stat` already follow symlinks transparently at the OS level. **3. File watcher** — The bundled watcher library (`tiny-readdir`) supports a `followSymlinks` option, but it was never passed through. Symlinked directories were detected but not traversed, so changes inside them didn't trigger watch events. Fixed by passing `followSymlinks: true` from `Watcher.js` and threading the option through `utils.readdir` → `tiny-readdir` at both call sites in `watcher_handler.js`. ## How have you tested this? - All 335 existing tests pass (`npm test`) - Updated the `recurseFiles` test mock to handle trailing slashes (since `realPath: false` no longer strips them) - Manual testing with directory symlinks for library folder selection, scanning, and file watching ## Screenshots N/A — server-side changes only --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
adam added the pull-request label 2026-04-25 00:19:46 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf#4444