aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common
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-common
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-common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/paths.py24
-rw-r--r--packages/meshbay-common/tests/test_paths.py49
2 files changed, 64 insertions, 9 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/paths.py b/packages/meshbay-common/src/meshbay_common/paths.py
index 8be4680..059bf09 100644
--- a/packages/meshbay-common/src/meshbay_common/paths.py
+++ b/packages/meshbay-common/src/meshbay_common/paths.py
@@ -19,7 +19,7 @@ from __future__ import annotations
import os
import re
import unicodedata
-from pathlib import Path, PurePath
+from pathlib import Path
# Windows refuses these as a basename, with or without an extension: `AUX.txt`
# is as impossible as `AUX`. A group indexed on Linux can hold them, and a
@@ -39,6 +39,9 @@ _RESERVED_CHARS = set('<>:"/\\|?*') | {chr(c) for c in range(32)} | BIDI_CONTROL
# Windows without long-path support. A deep media library reaches this.
MAX_PATH_WINDOWS = 260
+# Creating a directory is refused twelve characters earlier (room for an 8.3
+# name), and listing one adds `\*`: the extended form starts there.
+_EXTENDED_FROM = MAX_PATH_WINDOWS - 12
def nfc(text: str) -> str:
@@ -102,19 +105,22 @@ def long_path(path: Path | str) -> str:
"""
A path string safe to hand to the OS.
- On Windows, prefix with `\\\\?\\` so `MAX_PATH` does not truncate a deep
- library. The prefix requires an absolute, already-normalized path, and it is
- a no-op everywhere else.
+ On Windows, a path that reaches `MAX_PATH` is refused by every call unless
+ the system's `LongPathsEnabled` is set, which it is not by default and which
+ a program cannot set for itself. The extended form (`\\\\?\\`) is accepted
+ whatever the setting, so a long path gets it. The form skips the system's
+ normalisation, so the path is normalised here first. A no-op everywhere
+ else, and for a path short enough to need nothing.
"""
text = str(path)
if os.name != "nt" or text.startswith("\\\\?\\"):
return text
- resolved = str(PurePath(text))
- if len(resolved) < MAX_PATH_WINDOWS and not text.startswith("\\\\"):
+ full = os.path.normpath(text) if os.path.isabs(text) else os.path.abspath(text)
+ if len(full) < _EXTENDED_FROM:
return text
- if text.startswith("\\\\"): # UNC: \\server\share → \\?\UNC\server\share
- return "\\\\?\\UNC" + text[1:]
- return "\\\\?\\" + text
+ if full.startswith("\\\\"): # UNC: \\server\share → \\?\UNC\server\share
+ return "\\\\?\\UNC" + full[1:]
+ return "\\\\?\\" + full
def find_fold_collisions(names: list[str]) -> dict[str, list[str]]:
diff --git a/packages/meshbay-common/tests/test_paths.py b/packages/meshbay-common/tests/test_paths.py
index 012a32e..cbaf1f5 100644
--- a/packages/meshbay-common/tests/test_paths.py
+++ b/packages/meshbay-common/tests/test_paths.py
@@ -6,12 +6,15 @@ these are not compatibility niceties — they decide whether two index entries a
one file, and whether a member can save what they downloaded.
"""
+import os
+
import pytest
from meshbay_common.paths import (
find_fold_collisions,
fold,
fold_path,
is_portable_name,
+ long_path,
nfc,
portable_name_problem,
sanitize_for_download,
@@ -126,3 +129,49 @@ def test_a_bidi_override_cannot_hide_an_extension():
disguised = "invoice‮fdp.exe" # displays as "invoiceexe.pdf"
assert sanitize_for_download(disguised) == "invoice_fdp.exe"
assert portable_name_problem(disguised)
+
+
+# ── Long paths on Windows ────────────────────────────────────────────────────
+
+on_windows = pytest.mark.skipif(os.name != "nt", reason="the extended form is Windows'")
+BS = chr(92)
+EXT = BS * 2 + "?" + BS
+DEEP = BS.join(["C:", *["a folder name of some length"] * 9, "track.flac"])
+
+
+@on_windows
+def test_a_short_path_is_left_as_it_is():
+ assert long_path(f"C:{BS}Music{BS}track.flac") == f"C:{BS}Music{BS}track.flac"
+
+
+@on_windows
+def test_a_long_path_gets_the_extended_form():
+ assert long_path(DEEP) == EXT + DEEP
+
+
+@on_windows
+def test_the_extended_form_is_normalised_first():
+ """The system normalises nothing behind the prefix: a forward slash or a
+ `..` left in would name a file that does not exist."""
+ messy = DEEP.replace(BS, "/").replace("/track.flac", "/x/../track.flac")
+ assert long_path(messy) == EXT + DEEP
+
+
+@on_windows
+def test_a_long_share_path_gets_the_unc_form():
+ share = BS * 2 + "server" + BS + "share" + DEEP[2:]
+ assert long_path(share) == EXT + "UNC" + BS + share[2:]
+
+
+@on_windows
+def test_a_path_in_the_extended_form_is_left_alone():
+ assert long_path(EXT + DEEP) == EXT + DEEP
+
+
+@on_windows
+def test_a_directory_gets_it_before_max_path():
+ """Creating a directory is refused twelve characters before MAX_PATH."""
+ at_248 = "C:" + BS + "d" * 245
+ assert len(at_248) == 248
+ assert long_path(at_248) == EXT + at_248
+ assert long_path(at_248[:-1]) == at_248[:-1]