Fixed dead network mounts freezing folder listing in Browser.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-13 18:43:23 +02:00
1 parent 4c05e62949
commit 1f3913bb56
1 file changed
+31 -11
+31 -11
View File
@@ -277,6 +277,8 @@ class Browser(ft.Row):
# entries make the cache meaningless.
self.ColumnCells: dict[str, list[ft.Container]] = {}
self.MetadataCache: dict[Path, tuple[float, dict[str, str]]] = {}
# Filled by _BuildEntries from the directory listing itself, see _IsDir.
self.EntryIsDir: dict[Path, bool] = {}
self.FolderIconOverrides: dict[Path, FolderIconOverride] = LoadFolderIconOverrides()
# Bumped by _RefreshEntries every time it rebuilds the list, so a
# background _PopulateEntriesAsync pass from a superseded build can
@@ -1138,12 +1140,14 @@ class Browser(ft.Row):
blocking on it. _PopulateEntriesAsync fills the real values in
afterwards, off the UI thread. A cache hit (e.g. a same-directory
refresh after a rename) still shows immediately, no flicker."""
Cached = self.MetadataCache.get(Entry)
if Cached is None:
return {} # don't stat on a miss: a dead network mount blocks for ~20s
try:
Mtime = Entry.stat().st_mtime
except OSError:
return {}
Cached = self.MetadataCache.get(Entry)
if Cached is not None and Cached[0] == Mtime:
if Cached[0] == Mtime:
return Cached[1]
return {}
@@ -1197,7 +1201,7 @@ class Browser(ft.Row):
pass resolves the real icon. Identical to _LeadingFor's non-
thumbnail branches, so only images visibly change once their
thumbnail loads — everything else never flickers."""
if Entry.is_dir():
if self._IsDir(Entry):
return self._ResolveFolderIconFast(Entry, Size)
return ft.Icon(ft.Icons.INSERT_DRIVE_FILE_OUTLINED, size=Size, color=Palette.Overlay0)
@@ -1273,7 +1277,7 @@ class Browser(ft.Row):
NameKey = Entry.name.lower()
if Field == "Size":
try:
Primary = 0 if Entry.is_dir() else Entry.stat().st_size
Primary = 0 if self._IsDir(Entry) else Entry.stat().st_size
except OSError:
Primary = 0
elif Field == "Modified":
@@ -1282,16 +1286,24 @@ class Browser(ft.Row):
except OSError:
Primary = 0.0
elif Field == "Type":
Primary = "" if Entry.is_dir() else Entry.suffix.lower()
Primary = "" if self._IsDir(Entry) else Entry.suffix.lower()
else: # "Name"
Primary = NameKey
return (Primary, NameKey)
Reverse = not self.Config.SortAscending
Dirs = sorted((E for E in Entries if E.is_dir()), key=KeyFn, reverse=Reverse)
Files = sorted((E for E in Entries if not E.is_dir()), key=KeyFn, reverse=Reverse)
Dirs = sorted((E for E in Entries if self._IsDir(E)), key=KeyFn, reverse=Reverse)
Files = sorted((E for E in Entries if not self._IsDir(E)), key=KeyFn, reverse=Reverse)
return Dirs + Files
def _IsDir(self, Entry: Path) -> bool:
"""Entry.is_dir() answered from the type _BuildEntries' scandir already
read, so the synchronous build never stats an entry. A dead network
mount (e.g. a CIFS share whose host is down) blocks every stat for
~20s, which froze the whole listing for minutes."""
IsDir = self.EntryIsDir.get(Entry)
return Entry.is_dir() if IsDir is None else IsDir
def _BuildEntries(self, DirPath: Path) -> list[ft.Control]:
self.EntryTiles = {}
self.ColumnCells = {}
@@ -1300,7 +1312,15 @@ class Browser(ft.Row):
# next refresh, same as any other entry list change.
self.FolderIconOverrides = LoadFolderIconOverrides()
try:
Entries = list(DirPath.iterdir())
with os.scandir(DirPath) as Scan:
self.EntryIsDir = {}
for DirEntry in Scan:
try:
# Uses the listing's d_type, no stat (except for symlinks).
self.EntryIsDir[DirPath / DirEntry.name] = DirEntry.is_dir()
except OSError:
self.EntryIsDir[DirPath / DirEntry.name] = False
Entries = list(self.EntryIsDir)
except (PermissionError, FileNotFoundError):
self.CurrentEntries = []
return [ft.Text("Can't read this folder.", italic=True)]
@@ -1445,7 +1465,7 @@ class Browser(ft.Row):
),
expand=True,
)
if Entry.is_dir():
if self._IsDir(Entry):
# A directory row is both a drag source (like any other entry, so
# it can itself be moved elsewhere) and a drop target (so other
# entries can be dropped into it) — same MoveIntoDirectory Sidebar
@@ -1544,7 +1564,7 @@ class Browser(ft.Row):
on_secondary_tap_down=lambda e, p=Entry: self._RequestMenu(p, e),
),
)
if Entry.is_dir():
if self._IsDir(Entry):
# Same drop-target treatment as the list view's directory rows —
# see _BuildListRow.
return ft.DragTarget(
@@ -1555,7 +1575,7 @@ class Browser(ft.Row):
return GridDraggable
def _LeadingFor(self, Entry: Path, Size: int = 32):
if Entry.is_dir():
if self._IsDir(Entry):
return self._ResolveFolderIconFull(Entry, Size)
if IsThumbnailable(Entry):
ThumbnailPath = GetThumbnail(Entry, Size=(Size * 2, Size * 2))