Studio: expose Windows drive roots in the folder browser - #7082
danielhanchen merged 26 commits into
Conversation
The model-selection folder browser bounds navigation to the roots returned by _build_browse_allowlist(), which exposed Linux removable-media mounts via linux_run_media_mount_roots() but had no Windows analog. As a result a user on C: could not browse to D:/E: to pick a model directory. Add windows_drive_roots(), a Windows-only companion to linux_run_media_mount_roots() that lists readable logical drive roots, and wire it into both browse-allowlist builders and their suggestion chips so other drives are both navigable and offered as quick-picks. The helper is a no-op on Linux/macOS, so existing platforms are unaffected. Closes unslothai#6368
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Code Review
This pull request adds support for Windows drive roots in the folder browser, allowing users to navigate between logical drives (such as C: and D:) on Windows. The changes integrate these drive roots into the folder browser's allowlist and suggestion chips, and include a new suite of unit tests. Feedback suggests optimizing the drive-probing logic by using GetLogicalDrives via ctypes to prevent synchronous blocking of the event loop on disconnected network drives, and updating the test stubs to mock this API for deterministic test execution on Windows hosts.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
I am having trouble creating individual review comments. Click here to see my feedback.
studio/backend/utils/paths/external_media.py (117-139)
Probing all 26 drive letters synchronously on the event loop using os.path.isdir can cause severe performance bottlenecks on Windows. Specifically, if there are disconnected or offline network drives (UNC paths mapped to drive letters), os.path.isdir can block the thread for up to 30 seconds per drive, hanging the entire FastAPI backend.
To prevent this, we can query the active logical drives using GetLogicalDrives via ctypes first. This is an extremely fast, non-blocking OS-level call that allows us to skip probing non-existent or inactive drive letters entirely.
if platform.system() != "Windows":
return []
# Query active logical drives on Windows to avoid slow timeouts or system dialogs
# when probing non-existent, unmounted, or disconnected drives.
bitmask = 0
try:
import ctypes
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
except Exception:
pass
roots: list[Path] = []
seen: set[str] = set()
for letter in drive_letters:
letter = letter.strip().rstrip(":").upper()
if len(letter) != 1 or letter not in string.ascii_uppercase:
continue
if bitmask:
drive_idx = ord(letter) - ord("A")
if not (bitmask & (1 << drive_idx)):
continue
root_text = f"{letter}:\\"
try:
if not os.path.isdir(root_text):
continue
except OSError:
continue
if not os.access(root_text, os.R_OK):
continue
key = os.path.normcase(root_text)
if key in seen:
continue
seen.add(key)
roots.append(Path(root_text))
return rootsstudio/backend/tests/test_windows_external_drive_paths.py (11-17)
When running unit tests on a Windows host, the real GetLogicalDrives API will be called and return the host's actual drive layout, which will conflict with the stubbed existing_drives and cause the tests to fail. We should mock GetLogicalDrives in _stub_windows to ensure tests are completely deterministic across all platforms.
def _stub_windows(monkeypatch, existing_drives):
"""Simulate Windows exposing only *existing_drives* (e.g. {"C", "D"}) as
readable drive roots, so the test does not depend on the host's real FS."""
monkeypatch.setattr(external_media.platform, "system", lambda: "Windows")
present = {f"{d.upper()}:\\" for d in existing_drives}
monkeypatch.setattr(external_media.os.path, "isdir", lambda p: str(p) in present)
monkeypatch.setattr(external_media.os, "access", lambda p, _mode: str(p) in present)
# Also mock GetLogicalDrives if running on a Windows host to prevent real drives from interfering
try:
import ctypes
if hasattr(ctypes, "windll"):
mask = sum(1 << (ord(d.upper()) - ord("A")) for d in existing_drives)
monkeypatch.setattr(ctypes.windll.kernel32, "GetLogicalDrives", lambda: mask)
except Exception:
pass
…n test Add an allowlist integration test mirroring the Linux side's test_legacy_browse_allowlist_includes_linux_run_media_mounts: it extracts _build_browse_allowlist from routes/models.py, stubs external_media so windows_drive_roots() yields a fake drive root, and asserts that root becomes browsable through the built allowlist. Proves the wiring, not just the helper.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58aa8e3d00
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| _add(Path.home()) | ||
| for p in linux_run_media_mount_roots(): | ||
| _add(p) | ||
| for p in windows_drive_roots(): |
There was a problem hiding this comment.
Fix drive-root descendants in legacy browser
When /api/models/browse-folders runs on Windows, this new allowlist root is stored as D:\; _is_path_inside_allowlist() later checks descendants with target_real.startswith(root_real + os.sep), which becomes D:\\, so a child such as D:\models is rejected after _match_browse_child resolves it. The suggestion can open the drive root, but clicking any subfolder under that drive root returns 403 in this route; handle root paths with a trailing separator or use commonpath as the hub browser does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and kept fixed in af0cc8b. _is_path_inside_allowlist now uses os.path.splitdrive + os.path.commonpath, so a drive root (D:) authorizes its descendants (D:\models) via component-wise containment instead of a string prefix. Covered by test_windows_external_drive_paths.py (drive-root descendant, cross-drive, case-insensitive).
Resolve active logical drives from GetLogicalDrives() before probing each letter with os.path.isdir. Probing a drive letter mapped to a disconnected network share can otherwise block the async backend for tens of seconds per letter. The call degrades gracefully (falls back to probing all letters) when ctypes/windll is unavailable, so behavior is unchanged on Linux/macOS. Tests override the bitmask source to stay deterministic on real Windows hosts.
|
Thanks for the review. Addressed the drive-probing concern: |
for more information, see https://pre-commit.ci
routes/models.py _is_path_inside_allowlist() checked descendants with
startswith(root_real + os.sep). A drive root ("D:\") already ends in a
separator, so the prefix became "D:\\" and a child like "D:\models" was
rejected with 403 after the browser opened the drive root. Only append a
separator when the root does not already end in one. folder_browser.py already
uses commonpath and was unaffected. Adds a regression test covering the
separator-terminated-root descendant case.
|
Good catch on the descendant check. Fixed in |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c58a449fd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if key in seen: | ||
| continue | ||
| seen.add(key) | ||
| roots.append(Path(root_text)) |
There was a problem hiding this comment.
Preserve the system-prefix denylist when adding drive roots
When Windows exposes C: (or whichever drive hosts SystemRoot/ProgramFiles), this returns the whole drive root and both browse allowlists add it directly. I checked routes/models.py::_resolve_browse_target and hub/services/models/folder_browser.py::_resolve_browse_target: after this root matches they only block credential/config components, while storage/studio_db.py and hub/storage/scan_folders.py explicitly reject SystemRoot/ProgramFiles, so requests like /api/models/browse-folders?path=C:\Windows or the hub equivalent can now enumerate directories that the existing scan-folder policy denies. Filter those denied prefixes during browsing, or avoid adding system-drive roots wholesale.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, fixed in af0cc8b. Instead of filtering at drive-root construction, both browse resolvers now re-apply the system-prefix denylist through a new is_denied_system_path() in storage/studio_db.py and hub/storage/scan_folders.py, mirroring add_scan_folder (including the /run/media carve-out and Windows normcase). C:\Windows and C:\Program Files are refused on descent while C:\ stays browseable, and they are also hidden from the listing and chips. Covered by test_browse_denylist.py.
| root_prefix = root_real if root_real.endswith(os.sep) else root_real + os.sep | ||
| if target_real == root_real or target_real.startswith(root_prefix): |
There was a problem hiding this comment.
Keep POSIX root registrations from unlocking every child
On POSIX, when the legacy browse allowlist contains / (for example from an existing scan_folders row, since storage.studio_db.add_scan_folder does not reject the filesystem root), this new prefix becomes /, so every absolute path now passes _is_path_inside_allowlist. Before this change the descendant prefix was //, which only allowed browsing / itself; now /etc, /proc, etc. can be entered through /api/models/browse-folders even though those prefixes are otherwise denied by the scan-folder policy. Special-case Windows drive roots instead of any separator-terminated root.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, fixed in af0cc8b. _is_path_inside_allowlist now special-cases a bare POSIX filesystem root (empty splitdrive and dirname == path): it matches / itself but does not authorize descendants, so /etc is no longer reachable via a / scan-folder row. The browse-time denylist is a second gate for stale rows, and add_scan_folder now also rejects registering the filesystem root. Regression tests in test_browse_denylist.py.
Exposing whole Windows drive roots (and any legacy-registered filesystem root) widened the browse allowlist above system directories, but the browse resolvers only re-applied the credential/config denylist, not the _denied_path_prefixes() system-dir denylist that scan-folder registration enforces. That let browse-folders enumerate C:\Windows, C:\Program Files, /etc and /proc. - Add is_denied_system_path() to both storage modules and enforce it in both browse resolvers (legacy routes/models.py and hub folder_browser.py), on each resolved child and on the final target, keeping the /run/media carve-out. - Rework the legacy _is_path_inside_allowlist to use splitdrive + commonpath so a Windows drive root authorizes its descendants while a bare POSIX / does not, and to compare case-insensitively like the hub browser. - Reject the filesystem root in the legacy add_scan_folder, matching the hub. - Hide denied system dirs from browse listings and suggestion chips. - Add tests/test_browse_denylist.py and update the external-media path tests.
for more information, see https://pre-commit.ci
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Per-OS unit suites (ubuntu/macos run the POSIX-oriented tests; windows runs the cross-platform drive-root suite), a real Windows GetLogicalDrives + system-denylist integration on windows-latest, and a before/after security probe showing the /etc-via-/ hole existed before the fix and is closed after.
The browse-time denylist tests used real /etc and tmp_path locations; on macOS tmp lives under the (legitimately denied) /private/var and /etc resolves to /private/etc, so three tests failed there. Pin the platform / use a tmp-based denied prefix so they assert the same behavior on Linux, macOS and Windows.
|
@codex review |
1 similar comment
|
@codex review |
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The _is_path_inside_allowlist guard that stops a legacy-registered '/' scan folder from authorizing every absolute path lived only in the legacy browser. The hub browser used commonpath without it, so a stale '/' row let it descend into /var, /root, /home -- which the system-directory denylist (/proc /sys /dev /etc /boot /run) does not cover, while the legacy browser blocked them. Mirror the legacy guard so both browsers treat '/' identically. Also resolve each directory entry before the denylist check in both listing loops, so a symlink or junction pointing into a denied dir is hidden instead of rendered as a row that 403s on descent. Adds legacy-vs-hub parity tests.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc8f2cb68c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if os.path.dirname(normalized) == normalized: | ||
| raise ValueError("The filesystem root cannot be registered") |
There was a problem hiding this comment.
Allow UNC share roots to be registered
On Windows, a UNC share root such as \\nas\models also satisfies os.path.dirname(normalized) == normalized, so this new guard rejects it as a filesystem root. That blocks users whose model directory is the root of a network share from adding it through /api/models/scan-folders, even though it is not above local system directories like C:\Windows; please distinguish bare drive/filesystem roots from UNC share roots.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks. I did try exactly this: a carve-out that allowed a bare UNC share root while still rejecting C:\ and /. The follow-up review found it was only half-complete though. \\nas\models\ with its usual trailing separator was still rejected, and even once registered the share root was unbrowsable because ntpath.commonpath raises for the no-trailing-separator form that realpath stores. Fully supporting a bare share root means adding UNC-specific normalization to the security-critical containment path, which I cannot validate off Windows, so I reverted to rejecting it consistently with C:\ and /. Registering a subdirectory of the share (for example \\nas\models\gguf) still works and browses normally, which covers the real use case.
…y pass on macOS test_browse_folders_route.py exercises allowlist containment and the file-vs-directory guard, not the system-directory denylist. On macOS pytest tmp_path resolves under /private/var, a denied prefix, so _resolve_browse_target 403s the fixture dirs before the containment logic runs (4 failures). Add an autouse fixture that makes is_denied_system_path inert in this file; the denylist keeps its own coverage in test_browse_denylist.py.
|
@codex review |
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea09a31fe6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if os.path.dirname(normalized) == normalized: | ||
| raise ValueError("The filesystem root cannot be registered") |
There was a problem hiding this comment.
Do not reject UNC share roots as filesystem roots
On Windows, a UNC share root such as \\nas\models also satisfies ntpath.dirname(normalized) == normalized, so this new guard rejects a readable network share root before the existing denylist logic runs. That blocks users from registering a share root that was valid before and does not expose local drive system directories; the same guard was added in hub/storage/scan_folders.py, so both scan-folder endpoints need the narrower check (e.g. only POSIX / and drive roots like C:\).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 1983329. The register guard now calls is_local_filesystem_root, which still rejects a bare local root (/ or C:) but exempts a UNC share root (\server\share): it has no denied system dirs under it and was registerable before this guard was added. Applied to both the legacy (studio_db.py) and hub (scan_folders.py) browsers through one shared helper, with a parametrized ntpath/posixpath regression test.
| thread = threading.Thread(target = _probe, args = (path,), daemon = True) | ||
| thread.start() |
There was a problem hiding this comment.
Avoid accumulating timed-out drive probe threads
When a disconnected mapped drive makes os.path.isdir() block past _DRIVE_PROBE_TIMEOUT_S, this code returns without joining the daemon thread, but both browse endpoints call windows_drive_roots() on every folder request. Repeated navigation with one stale mapping can therefore spawn another stuck probe thread per request until the OS calls eventually return, causing unbounded thread buildup even though each individual request times out promptly; cache/serialize in-flight probes or otherwise avoid starting a fresh thread for the same hung drive each time.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Leaving this as is. A probe that blocks on a disconnected mapping is bounded by the OS: roughly 21s to abandon a TCP connect to an unreachable host, or the SMB SessTimeout (default about 60s) at the session layer, after which the daemon thread returns and exits. Stuck threads therefore self-drain to a steady state of about browse_rate times that timeout rather than growing without limit, and the returned root set is already correct because a timed-out drive is dropped. Running the isdir check in a bounded worker thread is the recommended mitigation for the redirector stall, so the current design is intentional.
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1983329e9b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if _pathmod.dirname(path) != path: | ||
| return False | ||
| drive, _ = _pathmod.splitdrive(path) | ||
| return drive[:2] not in ("\\\\", "//") |
There was a problem hiding this comment.
Reject device-prefixed drive roots as local roots
When a Windows path uses the Win32 device prefix (\\?\C:\ or \\.\C:\), ntpath.splitdrive() returns a drive beginning with \\, so this UNC carve-out treats it as non-local even though it is a local drive root. In that scenario add_scan_folder() accepts the root as a scan folder; once it is in the browse allowlist, paths like \\?\C:\Windows are contained by that root and also miss the normal C:\Windows denylist prefix, so the system-directory block can be bypassed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 3911798. is_local_filesystem_root now collapses the Windows device / extended-length prefix before classifying, so \?\C:\ and \.\C:\ are judged as the C:\ drive root and rejected, while \?\UNC\server\share still maps to the \server\share share root and stays registerable. Added parametrized ntpath cases for both forms.
| thread = threading.Thread(target = _probe, args = (path,), daemon = True) | ||
| thread.start() |
There was a problem hiding this comment.
Bound accumulated drive-probe threads
When a mapped drive stays hung longer than the timeout, each /browse-folders request starts a fresh daemon thread for that drive and returns without joining or cancelling it. With a disconnected network drive and repeated folder-browser navigation, these stuck probes accumulate until the OS calls eventually return, so the request timeout still allows unbounded thread/resource growth in the server process.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Leaving this as is, same as the earlier thread on this function. A probe that blocks on a disconnected mapping is bounded by the OS: roughly 21s to abandon a TCP connect to an unreachable host, or the SMB SessTimeout (default about 60s) at the session layer, after which the daemon thread returns and exits. Stuck threads therefore self-drain to a steady state of about browse_rate times that timeout rather than growing without bound, and the returned root set is already correct because a timed-out drive is dropped. Running the isdir check in a bounded worker thread is the recommended mitigation for the redirector stall, so I would rather not add a module-level probe cache and its locking to the hot browse path.
| # match; do not let it authorize arbitrary descendants. | ||
| continue | ||
| try: | ||
| if os.path.commonpath([target_real, root_real]) == root_real: |
There was a problem hiding this comment.
Preserve UNC share roots when checking containment
When a registered Windows UNC share is stored as \\server\share (no trailing slash), ntpath.commonpath(["\\server\share\models", "\\server\share"]) raises ValueError, so this containment check returns false and browsing any child of the newly allowed UNC root 403s. This regresses the commit's UNC-share exception unless the user happens to register the share with a trailing separator.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 3911798. os.path.commonpath raises 'can't mix absolute and relative' on a bare \server\share, so _is_path_inside_allowlist now detects a UNC share root (drive set, empty splitdrive tail) and authorizes its descendants with a boundary-safe prefix test on the normcased realpath. Applied to both the legacy and hub browsers, with a parametrized test that a registered share authorizes its children but not a sibling share or a different volume.
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac525776f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if path[:4].lower() in ("\\\\?\\", "\\\\.\\"): | ||
| rest = path[4:] | ||
| path = "\\\\" + rest[4:] if rest[:4].lower() == "unc\\" else rest | ||
| if _pathmod.dirname(path) != path: |
There was a problem hiding this comment.
Keep volume GUID roots classified as local roots
When a Windows path uses a volume/device root such as \\?\Volume{GUID}\, this prefix-stripping turns it into Volume{GUID}\, so _pathmod.dirname(path) != path and is_local_filesystem_root() returns False. add_scan_folder() can then accept a whole local volume even though drive roots are supposed to be rejected; because the denylist only matches drive-letter paths like C:\Windows, browsing that registered volume can expose system directories on the OS volume. Treat \\?\Volume.../\\.\Volume... as local filesystem roots before stripping or add an explicit volume-GUID case.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 86355eb. is_local_filesystem_root now resolves the device namespace fully: after a \?\ or \.\ prefix, a bare volume specifier with no further component (a drive letter C: or a Volume{GUID}) is a local volume root and rejected, while \?\UNC\server\share still maps to the \server\share share root and stays registerable. Added parametrized \?\Volume{GUID}\ and deep extended-length path cases.
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Tested this on a real Windows runner via a throwaway CI run on my fork: https://github.com/oobabooga/unsloth/actions/runs/29367849225. windows_drive_roots() picked up the runner's real C: and D:. Browsing C:\ hid Windows and Program Files, descent into C:\Windows 403'd, the drive chip appeared, and registering C:\ was rejected in both browsers. 8.3 aliases (PROGRA~1) resolve to their denied long form, \?\ device-namespace requests are refused, and your mocked Windows tests pass on the real host too. On POSIX nothing moved: a before/after harness over 27 containment cases shows zero drift from the commonpath rewrite, and 470 tests pass locally. One note: the Linux-simulation suites fail on a Windows host because their fixture paths hit the real os.path. CI runs them on ubuntu only, so nothing blocks; a skipif(os.sep != "/") can land separately. LGTM. @danielhanchen good to merge on my end. |
Summary
On Windows, Unsloth Studio's model-selection folder browser cannot navigate between drive letters: a user whose home is on
C:cannot browse toD:/E:to pick a model directory. Navigation is bounded to the roots returned by_build_browse_allowlist(), which exposes Linux removable-media mounts vialinux_run_media_mount_roots()but has no Windows analog, so only roots on the home drive are ever reachable (and a drive root's parent is itself, so the "up" control can't reach a drive picker).Changes
windows_drive_roots()tostudio/backend/utils/paths/external_media.py— a Windows-only companion tolinux_run_media_mount_roots()that lists readable logical drive roots (C:\,D:\, ...). Absent/unreadable drives are skipped; returns[]off Windows.hub/services/models/folder_browser.pyandroutes/models.py) and their suggestion-chip lists, next to the existing Linux loop, so other drives are both navigable and offered as quick-picks.Tests
studio/backend/tests/test_windows_external_drive_paths.py, mirroring the existingtest_linux_external_media_paths.pyplatform-mock style: no-op off Windows, lists readable drives, skips absent/unreadable drives, ignores malformed letters, and dedupes.external_mediastub to provide the new helper.python -m pytest tests/test_windows_external_drive_paths.py tests/test_linux_external_media_paths.py(fromstudio/backend) → 31 passed.Validation
ruff check(0.15.12, repo config),python -m compileall, andscripts/verify_import_hoist.py: all clean.Closes #6368