aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-14 11:17:38 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-14 11:17:38 +0200
commitdaec4bc6a2a9f9e038e5269586b06018ff327a27 (patch)
treec34c58aad624d594dbb1aa8a47f71ab38a5e3081 /packages/meshbay-node/src/meshbay_node/indexer/indexer.py
parent80ac882346b474bb7ea4e8c8afd4694bd13dd9dd (diff)
downloadmeshbay-daec4bc6a2a9f9e038e5269586b06018ff327a27.tar.gz
fix(node): index, serve and write files past Windows' MAX_PATHHEADmain
With the system's default setting (LongPathsEnabled off) every call given a plain path of 260 characters or more refuses it, so a deep library was partly invisible: the walk could not list such a directory and its files never reached the members. The node now hands each call that touches a group's disk the extended form (paths.long_path), and keeps the plain path in the index, the hash cache and the log. A directory is listed under its long form and its entries named from the plain one (indexer.walk_files, wire._walk_dirs). Covered: the walk, stats and hashing, reconcile and the watcher, the folder list members see, serving over WebRTC and QUIC, deletion, uploads and their recovery, folder creation and removal, audio tags and the cover beside a track, photos, and a show's episode numbering. ffmpeg extends a long path itself and keeps the plain one. long_path now normalises before prefixing (the extended form skips the system's normalisation) and starts at 248 characters, where creating a directory is already refused. test_long_paths.py runs with the setting off: 15 of its 17 tests fail without the change. Checked by hand in the installed app with files at 312 to 380 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/indexer.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py39
1 files changed, 26 insertions, 13 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index e4679d4..55ec56a 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -82,7 +82,7 @@ def _is_indexable_name(name: str) -> bool:
def _is_indexable(path: Path) -> bool:
- return path.is_file() and _is_indexable_name(path.name)
+ return os.path.isfile(long_path(path)) and _is_indexable_name(path.name)
# ── A disk that stops answering ───────────────────────────────────────────────
@@ -348,13 +348,25 @@ def _walk_root(root: Root) -> list[Path]:
every other thing the daemon is doing — WebRTC sessions, chat, the admin
UI — for as long as it takes.
"""
+ return walk_files(root.path)
+
+
+def walk_files(top: Path) -> list[Path]:
+ """
+ Every file below `top`, links to files included, links to directories not
+ entered. Raises if `top` itself cannot be listed. Blocking.
+
+ Each directory is listed under its long form and the paths returned are
+ built from the plain one, so a directory past Windows' MAX_PATH is listed
+ and everything downstream still sees, stores and compares plain paths.
+ """
files: list[Path] = []
- pending = [root.path]
+ pending = [top]
while pending:
directory = pending.pop()
_beat(directory)
try:
- with os.scandir(directory) as listing:
+ with os.scandir(long_path(directory)) as listing:
below: list[Path] = []
for entry in listing:
_beat()
@@ -363,13 +375,13 @@ def _walk_root(root: Root) -> list[Path]:
# a file counts whether or not it is one.
try:
if entry.is_dir(follow_symlinks=False):
- below.append(Path(entry.path))
+ below.append(directory / entry.name)
elif entry.is_file():
- files.append(Path(entry.path))
+ files.append(directory / entry.name)
except OSError:
continue
except OSError as e:
- if directory == root.path:
+ if directory == top:
raise
# One directory that cannot be listed costs that directory. It
# used to be silent for a refusal and to end the whole walk for
@@ -406,7 +418,7 @@ def _size_files(files: list[Path]) -> list[tuple[Path, os.stat_result]]:
for p in files:
_beat(p.parent)
try:
- sized.append((p, p.stat()))
+ sized.append((p, os.stat(long_path(p))))
except OSError:
continue
return sized
@@ -420,7 +432,7 @@ def _scan_file(root: Root, file_path: Path) -> IndexEntry | None:
if not _is_indexable(file_path):
return None
try:
- stat = file_path.stat()
+ stat = os.stat(long_path(file_path))
if not _is_indexable_size(file_path, stat.st_size):
return None
if stat.st_size > _PARTIAL_THRESHOLD:
@@ -903,7 +915,7 @@ class DirectoryIndexer:
if not _is_indexable(file_path):
return None
try:
- st = file_path.stat()
+ st = os.stat(long_path(file_path))
except OSError:
return None
elif not _is_indexable_name(file_path.name):
@@ -988,7 +1000,7 @@ class DirectoryIndexer:
if self._cache is None or not user_id:
return
try:
- st = file_path.stat()
+ st = os.stat(long_path(file_path))
except OSError:
# Gone between the rename and here. Nothing to attribute, and
# nothing for anyone to delete either.
@@ -1565,7 +1577,7 @@ class DirectoryIndexer:
return True
for path, size, mtime in known:
try:
- st = Path(path).stat()
+ st = os.stat(long_path(path))
except OSError:
continue
if st.st_size == size and (mtime is None or st.st_mtime == mtime):
@@ -1735,7 +1747,7 @@ class DirectoryIndexer:
# bookkeeping still has to reach the `fire()` that actually runs.
if not deleted and key not in self._burst_sizes:
try:
- size = file_path.stat().st_size
+ size = os.stat(long_path(file_path)).st_size
except OSError:
size = 0
if self._burst_inflight <= 0:
@@ -1790,7 +1802,8 @@ class DirectoryIndexer:
covers this directory too.
"""
try:
- return not dir_path.exists() and dir_path.parent.is_dir()
+ return (not os.path.exists(long_path(dir_path))
+ and os.path.isdir(long_path(dir_path.parent)))
except OSError:
return False