feat: shorten paths that exceed the device's limit
Rockbox's MAX_PATH is 260, defined in firmware/include/fs_defines.h and used to size the directory entry buffer in dir.h. It bounds the path as the device sees it, so the directory the mirror is copied into spends part of the same budget; --device-prefix accounts for that and defaults to /Music. Over-budget paths are shortened from the deepest component outward. The track name carries the least navigational value and the artist directory the most, so the filename is cut first and the artist only if nothing else will serve. A shortened component keeps its extension and gains four hex digits of the original name: two long titles sharing a prefix cut to the same string otherwise, and a silent collision between two tracks is a worse outcome than an ugly filename. The result is stable. The same source always yields the same shortened name, so one pass does not rename what the last one wrote -- an unstable scheme would churn the whole mirror every six hours. A path too deeply nested to fit without reducing every component to nonsense is left alone and reported rather than mangled. Migration now tries more than one previous naming, because there is more than one. A mirror already running with --fat32-safe holds sanitised but unshortened paths, and matching only the original unsanitised name would have re-encoded every one of them instead of moving it. The checker gains the same two options, since it was measuring the mirror-relative path against a limit that applies to the device-absolute one, and so under-reported by the length of the destination directory.
This commit is contained in:
@@ -613,3 +613,95 @@ def test_renames_are_counted_separately_from_encodes(tmp_path, make_flac, caplog
|
||||
|
||||
assert "1 renamed" in caplog.text
|
||||
assert "0 encoded" in caplog.text
|
||||
|
||||
|
||||
def test_a_long_path_is_shortened_from_the_deepest_component(tmp_path, make_flac):
|
||||
"""The track name carries the least navigational value and the artist the
|
||||
most, so the filename is sacrificed before the album."""
|
||||
artist = "A" * 60
|
||||
album = "B" * 60
|
||||
title = "C" * 150
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
make_flac(source / artist / album / f"{title}.flac")
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "160", "--device-prefix", "/Music")
|
||||
|
||||
written = list(mirror.rglob("*.mp3"))
|
||||
assert len(written) == 1
|
||||
relative = written[0].relative_to(mirror)
|
||||
assert relative.parts[0] == artist, "the artist directory should be untouched"
|
||||
assert relative.parts[1] == album, "the album directory should be untouched"
|
||||
assert len(str(relative)) <= 160 - len("Music") - 2
|
||||
|
||||
|
||||
def test_shortening_is_stable_across_passes(tmp_path, make_flac):
|
||||
"""An unstable name would rename every file on every pass, for ever."""
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
make_flac(source / ("D" * 80) / ("E" * 80) / f"{'F' * 120}.flac")
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "180")
|
||||
first = sorted(str(p.relative_to(mirror)) for p in mirror.rglob("*.mp3"))
|
||||
stamp = next(mirror.rglob("*.mp3")).stat().st_mtime_ns
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "180")
|
||||
|
||||
assert sorted(str(p.relative_to(mirror)) for p in mirror.rglob("*.mp3")) == first
|
||||
assert next(mirror.rglob("*.mp3")).stat().st_mtime_ns == stamp
|
||||
|
||||
|
||||
def test_two_long_names_do_not_collide_after_shortening(tmp_path, make_flac):
|
||||
"""They share a prefix and cut to the same string; the hash is what keeps
|
||||
them apart."""
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
shared = "G" * 140
|
||||
make_flac(source / "Album" / f"{shared}one.flac")
|
||||
make_flac(source / "Album" / f"{shared}two.flac")
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "120")
|
||||
|
||||
assert len(list(mirror.rglob("*.mp3"))) == 2
|
||||
|
||||
|
||||
def test_a_sanitised_mirror_is_renamed_rather_than_re_encoded_when_shortening(
|
||||
tmp_path, make_flac
|
||||
):
|
||||
"""The previous naming is sanitised-but-not-shortened, not the original."""
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
make_flac(source / "Album" / f"Where Are You? {'H' * 140}.flac")
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "400")
|
||||
before = next(mirror.rglob("*.mp3"))
|
||||
contents = before.read_bytes()
|
||||
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "120")
|
||||
|
||||
after = next(mirror.rglob("*.mp3"))
|
||||
assert after != before
|
||||
assert after.read_bytes() == contents, "it was re-encoded rather than moved"
|
||||
|
||||
|
||||
def test_shortening_only_applies_when_over_budget(tmp_path, make_flac):
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
make_flac(source / "Artist" / "Album" / "Short Name.flac")
|
||||
|
||||
run(source, mirror, "--fat32-safe")
|
||||
|
||||
assert (mirror / "Artist" / "Album" / "Short Name.mp3").is_file()
|
||||
|
||||
|
||||
def test_a_path_that_cannot_be_made_to_fit_is_reported(tmp_path, make_flac, caplog):
|
||||
"""Too deeply nested to shorten without making every component unreadable."""
|
||||
deep = Path(*["I" * 20 for _ in range(10)])
|
||||
source = tmp_path / "src"
|
||||
mirror = tmp_path / "dst"
|
||||
make_flac(source / deep / "track.flac")
|
||||
|
||||
with caplog.at_level("WARNING"):
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "80")
|
||||
|
||||
assert "too" in caplog.text and "nested" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user